diff --git a/src/outbound-fetch-policy.ts b/src/outbound-fetch-policy.ts index 10f41fad6..e9c20e05c 100644 --- a/src/outbound-fetch-policy.ts +++ b/src/outbound-fetch-policy.ts @@ -69,8 +69,13 @@ function outboundHeaders(input: RequestInfo | URL, init: RequestInit | undefined } function outboundBodyPresent(input: RequestInfo | URL, init: RequestInit | undefined): boolean { - if (init && Object.prototype.hasOwnProperty.call(init, "body")) { - return init.body !== null && init.body !== undefined; + if ( + init + && Object.prototype.hasOwnProperty.call(init, "body") + && init.body !== null + && init.body !== undefined + ) { + return true; } return input instanceof Request && input.body !== null; } diff --git a/test/outbound-request-body-inheritance.test.ts b/test/outbound-request-body-inheritance.test.ts new file mode 100644 index 000000000..b4d422710 --- /dev/null +++ b/test/outbound-request-body-inheritance.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from "vitest"; +import { createFailClosedFetch } from "../src/outbound-fetch-policy"; + +const discoveryUrl = + "https://token.actions.githubusercontent.com/.well-known/openid-configuration"; + +describe("outbound request body inheritance", () => { + it.each([ + ["null", null], + ["undefined", undefined], + ] as const)( + "rejects an inherited Request body when RequestInit.body is %s", + async (_label, bodyOverride) => { + const rawFetch = vi.fn(async () => new Response("unexpected", { status: 200 })); + const guardedFetch = createFailClosedFetch(rawFetch); + const bodyfulRequest = new Request(discoveryUrl, { + method: "POST", + body: "credential-bearing payload", + }); + + const response = await guardedFetch(bodyfulRequest, { + method: "GET", + body: bodyOverride, + }); + + expect(response.status).toBe(502); + expect(response.headers.get("x-noema-egress-policy")).toBe( + "blocked-request-policy", + ); + expect(rawFetch).not.toHaveBeenCalled(); + }, + ); +}); diff --git a/test/outbound-request-compartment.test.ts b/test/outbound-request-compartment.test.ts index fa8073007..b0023769c 100644 --- a/test/outbound-request-compartment.test.ts +++ b/test/outbound-request-compartment.test.ts @@ -33,7 +33,7 @@ describe("outbound credential request compartmentalization", () => { }, ); - it("derives the effective request from Request input plus RequestInit overrides", () => { + it("does not treat null RequestInit body as clearing an inherited Request body", () => { const unsafeInput = new Request(discoveryUrl, { method: "POST", headers: { authorization: "Bearer sensitive" }, @@ -45,7 +45,7 @@ describe("outbound credential request compartmentalization", () => { method: "GET", headers: {}, body: null, - })).toBe(true); + })).toBe(false); }); it("allows public bodyless GitHub GETs and only the two reviewed App-JWT operations", () => {