fix(learner): SPA proxy forwards no credentials and never goes through HTTP_PROXY

spaWithCsp proxies GET and HEAD only (405 otherwise, no body buffering) and forwards only accept,
if-none-match, if-modified-since and range. The internal server listens on a Unix socket in a private
0700 temp dir. Bun 1.3.14 ignores proxy: false for this hop, and over a socket with a proxy set it sends
an absolute-form target that breaks asset routing, so localhost, 127.0.0.1 and ::1 are appended to
NO_PROXY/no_proxy (existing entries kept). accept-encoding is no longer forced; fetch decompresses and
the encoding headers are dropped.

Test: in a subprocess with HTTP(S)_PROXY and ALL_PROXY set to a dead proxy, the SPA and an asset load
with the CSP, POST gets 405, and the upstream sees no Cookie, Authorization or X-Forwarded-For.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-30 14:27:56 +01:00
1 parent c7b9e1f98e
commit 213f2247b4
3 files changed
+125 -12

No files matched your search

+22
View File
@@ -63,3 +63,25 @@ test("the runner frame is served sandboxed, with no connections and only its own
expect(scriptSrc(csp)).toMatch(/^script-src 'sha256-[A-Za-z0-9+/=]+' blob:$/);
await db.close();
});
test("the SPA proxy ignores HTTP_PROXY/ALL_PROXY and forwards no credentials", () => {
const dead = "http://127.0.0.1:9"; // nothing listens on the discard port
const p = Bun.spawnSync([process.execPath, new URL("./e2e/spa-proxy-check.ts", import.meta.url).pathname], {
env: { ...process.env, HTTP_PROXY: dead, http_proxy: dead, HTTPS_PROXY: dead, https_proxy: dead, ALL_PROXY: dead, all_proxy: dead, NO_PROXY: "corp.example", no_proxy: "" },
stdout: "pipe", stderr: "pipe",
});
expect(p.exitCode).toBe(0);
const out = JSON.parse(p.stdout.toString().trim().split("\n").pop()!);
// The environment proxy really is in effect for plain fetches in that process...
expect(out.controlViaEnvProxy).not.toBe("status 200");
// ...but the SPA still loads through spaWithCsp, with its CSP.
expect(out.status).toBe(200);
expect(out.body).toContain('<div id="root">');
expect(out.csp).toContain("script-src 'self'");
expect(out.post).toBe(405);
// Assets come through too (with a proxy in the env, Bun's socket fetch would otherwise hit the HTML fallback).
expect(out.asset).toBe("200 text/javascript console.log(1)");
expect(out.noProxy).toBe("corp.example,localhost,127.0.0.1,::1");
// The upstream saw only the allowlisted headers: no Cookie, Authorization or X-Forwarded-*.
expect(out.seen).toEqual([{ cookie: null, authorization: null, xff: null, accept: "text/html" }]);
}, 30_000);