Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/vs/workbench/api/common/extHostMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -862,7 +869,28 @@ 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.
// Fail closed so the connection errors deterministically rather than the
// caller treating the 3xx response as final.
if (!ALLOWED_REDIRECT_PROTOCOLS.has(nextUrlParsed.protocol)) {
throw new Error(`MCP server redirected to a non-http(s) target (${nextUrlParsed.protocol}), which is not allowed`);
}
Comment thread
g0w6y marked this conversation as resolved.

// 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];
}
}
Comment thread
g0w6y marked this conversation as resolved.
}

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.
Expand Down