diff --git a/packages/codev-sandbox/src/broker.test.ts b/packages/codev-sandbox/src/broker.test.ts index f1ef99fb13..c246953a18 100644 --- a/packages/codev-sandbox/src/broker.test.ts +++ b/packages/codev-sandbox/src/broker.test.ts @@ -16,7 +16,7 @@ test("resolves allowed prefixes onto the upstream and denies everything else", ( expect(resolveBrokerTarget(routes, "/etc/passwd")).toBe(null) }) -test("strips hop-by-hop and host headers before forwarding", () => { +test("strips hop-by-hop, host, and browser-controlled headers before forwarding", () => { expect( forwardableRequestHeaders({ host: "127.0.0.1:9999", @@ -25,6 +25,11 @@ test("strips hop-by-hop and host headers before forwarding", () => { authorization: "Bearer token", "x-api-key": "key", accept: undefined, + // Chromium refuses to send a request carrying these, so a host that + // forwards through Electron's net stack fails outright on them. + "sec-fetch-mode": "cors", + "Sec-Fetch-Site": "cross-site", + "sec-ch-ua": '"Chromium";v="1"', }), ).toEqual({ authorization: "Bearer token", "x-api-key": "key" }) }) diff --git a/packages/codev-sandbox/src/broker.ts b/packages/codev-sandbox/src/broker.ts index 84986e41b8..1f75c1d94f 100644 --- a/packages/codev-sandbox/src/broker.ts +++ b/packages/codev-sandbox/src/broker.ts @@ -77,10 +77,20 @@ const REQUEST_HEADER_DROP = new Set([ // body we no longer send. const RESPONSE_HEADER_DROP = new Set(["content-encoding", "content-length", "transfer-encoding", "connection"]) +// Browser-controlled headers a client may not set. A host that forwards with +// Chromium's network stack (the desktop's Electron net.fetch) rejects the +// entire request with net::ERR_INVALID_ARGUMENT if one is present, so they are +// dropped here rather than at one caller. +const REQUEST_HEADER_DROP_PREFIX = ["sec-fetch-", "sec-ch-", "proxy-"] + export function forwardableRequestHeaders(headers: Record) { return Object.fromEntries( Object.entries(headers) - .filter(([name, value]) => value !== undefined && !REQUEST_HEADER_DROP.has(name.toLowerCase())) + .filter(([name, value]) => { + if (value === undefined) return false + const lower = name.toLowerCase() + return !REQUEST_HEADER_DROP.has(lower) && !REQUEST_HEADER_DROP_PREFIX.some((p) => lower.startsWith(p)) + }) .map(([name, value]) => [name, Array.isArray(value) ? value.join(", ") : (value as string)]), ) }