diff --git a/packages/codeflow-mcp/src/invoke/index.test.ts b/packages/codeflow-mcp/src/invoke/index.test.ts index bcafe65..0c10954 100644 --- a/packages/codeflow-mcp/src/invoke/index.test.ts +++ b/packages/codeflow-mcp/src/invoke/index.test.ts @@ -225,6 +225,12 @@ describe("TOOLS registry", () => { * present. When `MCP_ALLOWED_ORIGIN` is set (strict mode), only allowlisted * origins are echoed; untrusted or missing origins fall back to the first * entry in the allowlist. + * + * The credential-bearing headers `authorization` and `x-api-key` are + * intentionally OMITTED from `Access-Control-Allow-Headers` so a cross-origin + * attacker cannot make the browser send them via preflight. Non-credential + * headers (`Content-Type`, `x-request-id`) are retained so legitimate clients + * can still issue preflight requests. */ describe("createHttpServer CORS handling", () => { let server: Server; @@ -358,7 +364,7 @@ describe("createHttpServer CORS handling", () => { expect(res.allowOrigin).toBe(origin); }); - it("still includes authorization in Access-Control-Allow-Headers (echoing Origin does not strip credentials headers)", async () => { + it("omits authorization and x-api-key from Access-Control-Allow-Headers to prevent cross-origin credential exposure", async () => { const res = await new Promise<{ allowHeaders: string | string[] | undefined }>((resolve, reject) => { const req = httpRequest( `${baseUrl}/`, @@ -375,8 +381,16 @@ describe("createHttpServer CORS handling", () => { req.end(); }); const allowHeaders = Array.isArray(res.allowHeaders) ? res.allowHeaders.join(",") : res.allowHeaders ?? ""; - expect(allowHeaders).toContain("authorization"); - expect(allowHeaders).toContain("x-api-key"); + const normalized = allowHeaders.toLowerCase(); + // Credential-bearing headers must NOT be advertised cross-origin: a wildcard + // (or echoed) Origin combined with these in Allow-Headers would let any + // malicious site make the browser send Authorization / X-API-Key to this + // endpoint via a preflight. + expect(normalized).not.toContain("authorization"); + expect(normalized).not.toContain("x-api-key"); + // Non-credential headers must still be present so legitimate preflights succeed. + expect(normalized).toContain("content-type"); + expect(normalized).toContain("x-request-id"); }); }); diff --git a/packages/codeflow-mcp/src/invoke/index.ts b/packages/codeflow-mcp/src/invoke/index.ts index 4daed44..1d98119 100644 --- a/packages/codeflow-mcp/src/invoke/index.ts +++ b/packages/codeflow-mcp/src/invoke/index.ts @@ -206,6 +206,11 @@ export async function startStdioServer(): Promise { * `process.env` is read at call time so tests can stub the variable. */ function buildCorsHeaders(requestOrigin?: string): Record { + // We omit `authorization` and `x-api-key` from Allow-Headers so the server + // does not advertise acceptance of credentialed headers. Combined with the + // explicit origin allowlist / echoed-Origin behaviour above, this prevents + // a cross-origin attacker from making the browser send Authorization / + // X-API-Key to this endpoint. const allowedEnv = (process.env["MCP_ALLOWED_ORIGIN"] ?? "").trim(); if (allowedEnv.length > 0) { const allowed = new Set( @@ -222,14 +227,14 @@ function buildCorsHeaders(requestOrigin?: string): Record { return { "Access-Control-Allow-Origin": allowOrigin, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, authorization, x-api-key, x-request-id", + "Access-Control-Allow-Headers": "Content-Type, x-request-id", }; } const allowOrigin = requestOrigin && requestOrigin.length > 0 ? requestOrigin : "*"; return { "Access-Control-Allow-Origin": allowOrigin, "Access-Control-Allow-Methods": "GET, POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, authorization, x-api-key, x-request-id", + "Access-Control-Allow-Headers": "Content-Type, x-request-id", }; }