From 5699f83807c550f21c5d256218af9d8569b08cf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 14:07:42 +0900 Subject: [PATCH 1/3] test(security): reject inherited outbound request bodies --- .../outbound-request-body-inheritance.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/outbound-request-body-inheritance.test.ts 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(); + }, + ); +}); From f189b58d840845fe0def9826a6ea87216db43f16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 14:10:12 +0900 Subject: [PATCH 2/3] fix(security): preserve inherited outbound request bodies --- src/outbound-fetch-policy.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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; } From 657e670e1e8b0e9fae9b3771b7ed2326ef31a7ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 14:12:47 +0900 Subject: [PATCH 3/3] test(security): align inherited request body contract --- test/outbound-request-compartment.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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", () => {