From b209f6104252d6591baa07dc19b3eb8bc913c227 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 13 Aug 2026 13:30:38 +0700 Subject: [PATCH] fix(sandbox): drop browser-controlled headers when forwarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The guest's model requests carry sec-fetch-mode (and friends), which a host forwarding through Chromium's network stack may not set: Electron's net.fetch rejected every such request with net::ERR_INVALID_ARGUMENT, so chat inside a sandbox failed on attempt after attempt while a plain curl through the same broker succeeded. Strip sec-fetch-*, sec-ch-*, and proxy-* alongside the existing hop-by-hop set. Header hygiene belongs in the broker rather than in one host's fetch wrapper, since any Chromium-backed forwarder hits this. Verified in-app: a sandbox session now answers "run pwd" with /home/codev — inside the guest, through the broker, no prompt. Co-Authored-By: Claude Fable 5 --- packages/codev-sandbox/src/broker.test.ts | 7 ++++++- packages/codev-sandbox/src/broker.ts | 12 +++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) 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)]), ) }