From a6be37b590404cbc37a318d89ed2f653eb51a192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=B3=95=E2=B2=9B=CF=84=E2=B2=89=E2=B2=85=E2=B2=A5?= =?UTF-8?q?=E2=B2=89=E2=B3=8F=CF=84=E2=B2=9F=E2=B2=85=20=F0=9F=95=B5?= =?UTF-8?q?=F0=9F=8F=BB?= <192411347+g0w6y@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:37:35 +0530 Subject: [PATCH 1/2] Validate redirect scheme and strip credentials on cross-origin redirects in MCP HTTP client --- src/vs/workbench/api/common/extHostMcp.ts | 29 ++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 65e87a5ebec5c..7d1dbc3b0e4b4 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -333,6 +333,13 @@ type HttpModeT = const MAX_FOLLOW_REDIRECTS = 5; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; +// MCP server URLs are restricted to http(s) at configuration time; the redirect +// path must enforce the same so a Location header cannot reach unix://, pipe://, +// file://, etc. +const ALLOWED_REDIRECT_PROTOCOLS = new Set(['http:', 'https:']); +// Credential-bearing headers that must not be replayed to a different origin +// after a redirect (matches browser fetch / curl behavior). Compared case-insensitively. +const CROSS_ORIGIN_STRIPPED_HEADERS = new Set(['authorization', 'cookie', 'proxy-authorization', 'mcp-session-id']); /** * Implementation of both MCP HTTP Streaming as well as legacy SSE. @@ -862,7 +869,27 @@ export class McpHTTPHandle extends Disposable { break; } - const nextUrl = new URL(location, currentUrl).toString(); + const currentUrlParsed = new URL(currentUrl); + const nextUrlParsed = new URL(location, currentUrl); + + // Only follow redirects to http(s). Blocks a malicious Location header from + // reaching the unix:// / pipe:// socket dispatcher or other local schemes. + if (!ALLOWED_REDIRECT_PROTOCOLS.has(nextUrlParsed.protocol)) { + this._log(LogLevel.Warning, `Refusing to follow MCP redirect to non-http(s) target (${nextUrlParsed.protocol})`); + break; + } + + // On a cross-origin redirect, strip credential-bearing headers so tokens and + // session ids configured for the original origin are not replayed to another host. + if (currentUrlParsed.origin !== nextUrlParsed.origin) { + for (const name of Object.keys(init.headers)) { + if (CROSS_ORIGIN_STRIPPED_HEADERS.has(name.toLowerCase())) { + delete init.headers[name]; + } + } + } + + const nextUrl = nextUrlParsed.toString(); this._log(LogLevel.Trace, `Redirect (${response.status}) from ${currentUrl} to ${nextUrl}`); currentUrl = nextUrl; // Per fetch spec, for 303 always use GET, keep method unless original was POST and 301/302, then GET. From 9e02b21861772c8bf73b4574825805a094aef174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=B3=95=E2=B2=9B=CF=84=E2=B2=89=E2=B2=85=E2=B2=A5?= =?UTF-8?q?=E2=B2=89=E2=B3=8F=CF=84=E2=B2=9F=E2=B2=85=20=F0=9F=95=B5?= =?UTF-8?q?=F0=9F=8F=BB?= <192411347+g0w6y@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:44:11 +0530 Subject: [PATCH 2/2] Fail closed on disallowed redirect scheme instead of breaking --- src/vs/workbench/api/common/extHostMcp.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 7d1dbc3b0e4b4..404c0893f4a7f 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -874,9 +874,10 @@ export class McpHTTPHandle extends Disposable { // Only follow redirects to http(s). Blocks a malicious Location header from // reaching the unix:// / pipe:// socket dispatcher or other local schemes. + // Fail closed so the connection errors deterministically rather than the + // caller treating the 3xx response as final. if (!ALLOWED_REDIRECT_PROTOCOLS.has(nextUrlParsed.protocol)) { - this._log(LogLevel.Warning, `Refusing to follow MCP redirect to non-http(s) target (${nextUrlParsed.protocol})`); - break; + throw new Error(`MCP server redirected to a non-http(s) target (${nextUrlParsed.protocol}), which is not allowed`); } // On a cross-origin redirect, strip credential-bearing headers so tokens and