From 7a53f1bad93a59ac494aae6c6d9724a5a6d82fcd Mon Sep 17 00:00:00 2001 From: Christopher Modjeska Date: Sun, 5 Jul 2026 01:36:54 -0400 Subject: [PATCH] fix: wildcard host (0.0.0.0) binding support When bound to 0.0.0.0, Host header checks are skipped and the request Origin is used as the allowed CORS origin. Localhost and 127.0.0.1 are added to allowed hosts in remote mode. getKanbanOauthRedirectOrigin maps 0.0.0.0 to 127.0.0.1 for MCP OAuth callback URLs. --- src/cline-sdk/cline-mcp-runtime-service.ts | 6 ++-- src/core/runtime-endpoint.ts | 6 ++++ src/server/middleware.ts | 41 ++++++++++++++-------- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/cline-sdk/cline-mcp-runtime-service.ts b/src/cline-sdk/cline-mcp-runtime-service.ts index 8607be8fe9..b88a3e81c4 100644 --- a/src/cline-sdk/cline-mcp-runtime-service.ts +++ b/src/cline-sdk/cline-mcp-runtime-service.ts @@ -15,7 +15,7 @@ import type { import { z } from "zod"; import type { RuntimeClineMcpServer } from "../core/api-contract"; -import { buildKanbanRuntimeUrl } from "../core/runtime-endpoint"; +import { getKanbanOauthRedirectOrigin } from "../core/runtime-endpoint"; import { lockedFileSystem } from "../fs/locked-file-system"; import { createClineMcpSettingsService, resolveMcpSettingsPath } from "./cline-mcp-settings-service"; import { @@ -440,7 +440,7 @@ class RuntimeMcpServerClient implements SdkMcpServerClient { serverName: this.server.name, redirectUrl: parseOauthSettings(this.oauthSettingsPath).servers[this.server.name]?.redirectUrl ?? - buildKanbanRuntimeUrl(OAUTH_CALLBACK_PATH), + getKanbanOauthRedirectOrigin() + OAUTH_CALLBACK_PATH, }); } @@ -546,7 +546,7 @@ class RuntimeMcpServerClient implements SdkMcpServerClient { } function buildMcpOauthCallbackUrl(requestId: string): string { - const callbackUrl = new URL(buildKanbanRuntimeUrl(OAUTH_CALLBACK_PATH)); + const callbackUrl = new URL(getKanbanOauthRedirectOrigin() + OAUTH_CALLBACK_PATH); callbackUrl.searchParams.set(OAUTH_CALLBACK_REQUEST_ID_PARAM, requestId); return callbackUrl.toString(); } diff --git a/src/core/runtime-endpoint.ts b/src/core/runtime-endpoint.ts index dff687b91c..46d92078da 100644 --- a/src/core/runtime-endpoint.ts +++ b/src/core/runtime-endpoint.ts @@ -106,6 +106,12 @@ export function getKanbanRuntimeOrigin(): string { return `${scheme}://${getKanbanRuntimeHost()}:${getKanbanRuntimePort()}`; } +export function getKanbanOauthRedirectOrigin(): string { + const scheme = isKanbanRuntimeHttps() ? "https" : "http"; + const host = runtimeHost === "0.0.0.0" ? "127.0.0.1" : runtimeHost; + return `${scheme}://${host}:${getKanbanRuntimePort()}`; +} + export function getKanbanRuntimeWsOrigin(): string { const scheme = isKanbanRuntimeHttps() ? "wss" : "ws"; return `${scheme}://${getKanbanRuntimeHost()}:${getKanbanRuntimePort()}`; diff --git a/src/server/middleware.ts b/src/server/middleware.ts index acbb134ad2..9c676824e2 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -70,6 +70,10 @@ export function getAllowedHostHeaders(): ReadonlySet { if (isKanbanRemoteHost()) { addHostPort(boundHost); + if (boundHost === "0.0.0.0") { + addHostPort("localhost"); + addHostPort("127.0.0.1"); + } return allowed; } @@ -109,18 +113,22 @@ function rejectSocket(socket: Duplex): { end: boolean } { } export function handleHttpRequest(req: IncomingMessage, res: ServerResponse): { end: boolean } { - const hostDecision = evaluateHost({ - hostHeader: req.headers.host, - allowedHosts: getAllowedHostHeaders(), - }); - if (hostDecision.kind === "reject") { - return rejectRequest(res, "Host not allowed."); + const boundHost = getKanbanRuntimeHost().toLowerCase(); + if (boundHost !== "0.0.0.0") { + const hostDecision = evaluateHost({ + hostHeader: req.headers.host, + allowedHosts: getAllowedHostHeaders(), + }); + if (hostDecision.kind === "reject") { + return rejectRequest(res, "Host not allowed."); + } } + const allowedOrigin = boundHost === "0.0.0.0" && req.headers.origin ? req.headers.origin : getKanbanRuntimeOrigin(); const corsDecision = evaluateCors({ method: req.method, originHeader: req.headers.origin, - allowedOrigin: getKanbanRuntimeOrigin(), + allowedOrigin, }); switch (corsDecision.kind) { @@ -146,18 +154,23 @@ export function handleHttpRequest(req: IncomingMessage, res: ServerResponse): { } export function handleSocketUpgrade(request: IncomingMessage, socket: Duplex): { end: boolean } { - const hostDecision = evaluateHost({ - hostHeader: request.headers.host, - allowedHosts: getAllowedHostHeaders(), - }); - if (hostDecision.kind === "reject") { - return rejectSocket(socket); + const boundHost = getKanbanRuntimeHost().toLowerCase(); + if (boundHost !== "0.0.0.0") { + const hostDecision = evaluateHost({ + hostHeader: request.headers.host, + allowedHosts: getAllowedHostHeaders(), + }); + if (hostDecision.kind === "reject") { + return rejectSocket(socket); + } } + const allowedOrigin = + boundHost === "0.0.0.0" && request.headers.origin ? request.headers.origin : getKanbanRuntimeOrigin(); const corsDecision = evaluateCors({ method: request.method, originHeader: request.headers.origin, - allowedOrigin: getKanbanRuntimeOrigin(), + allowedOrigin, }); if (corsDecision.kind === "reject") { return rejectSocket(socket);