diff --git a/.changeset/fix-serve-reject-origin-null.md b/.changeset/fix-serve-reject-origin-null.md new file mode 100644 index 00000000..4cbff62a --- /dev/null +++ b/.changeset/fix-serve-reject-origin-null.md @@ -0,0 +1,5 @@ +--- +"@stainless-code/codemap": patch +--- + +Reject opaque `Origin: null` in `codemap serve` CSRF checks. diff --git a/src/application/http-server.test.ts b/src/application/http-server.test.ts index 3a870510..9205ad51 100644 --- a/src/application/http-server.test.ts +++ b/src/application/http-server.test.ts @@ -579,14 +579,19 @@ describe("http-server — CSRF + DNS-rebinding guard", () => { expect(body.error).toContain("Origin"); }); - it("allows Origin: null (file:// pages, sandboxed iframes — non-attack vector)", async () => { + it("rejects Origin: null (opaque browser contexts)", async () => { serverHandle = await startServer(); - const r = await fetch(`http://127.0.0.1:${serverHandle.port}/tool/query`, { - method: "POST", - headers: { "Content-Type": "application/json", Origin: "null" }, - body: JSON.stringify({ sql: "SELECT 1" }), - }); - expect(r.status).toBe(200); + const r = await fetch( + `http://127.0.0.1:${serverHandle.port}/tool/save_baseline`, + { + method: "POST", + headers: { "Content-Type": "application/json", Origin: "null" }, + body: JSON.stringify({ name: "csrf-test", sql: "SELECT 1" }), + }, + ); + expect(r.status).toBe(403); + const body = (await r.json()) as { error: string }; + expect(body.error).toContain("Origin: null"); }); it("rejects POST with mismatched Host header (DNS rebinding)", async () => { diff --git a/src/application/http-server.ts b/src/application/http-server.ts index 02078dce..5e2d1361 100644 --- a/src/application/http-server.ts +++ b/src/application/http-server.ts @@ -571,7 +571,8 @@ function validate( * that interface. * 3. **`Origin`** — fallback for older browsers that don't send * `Sec-Fetch-Site`. Browsers send `Origin` on every non-GET request - * (and most GETs); non-browser clients don't. Reject if present. + * (and most GETs); non-browser clients don't. Reject if present, + * including the opaque value `null` (file://, sandboxed contexts). * * Returns a reason string (becomes the 403 body) or `undefined` to allow. */ @@ -601,7 +602,7 @@ function csrfCheck( } const origin = req.headers.origin; - if (origin !== undefined && origin !== "" && origin !== "null") { + if (origin !== undefined && origin !== "") { return `cross-origin request rejected (Origin: ${origin}). codemap serve does not accept browser-driven cross-origin requests.`; }