From cb393f5af54c2540b9006831782c12a6ef8ba425 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Sat, 23 May 2026 11:21:49 +1000 Subject: [PATCH 1/2] fix(proxy): strip Set-Cookie from upstream responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A paid API proxy must never let an upstream service set cookies in the user's browser under the proxy's own origin. Otherwise a compromised, misbehaving, or attacker-influenced upstream can return `Set-Cookie: session=evil; Domain=.example.com; Secure` and the browser will honor it for every sibling subdomain of the proxy — turning any future path-confusion or open-redirect bug in the surrounding deployment into a session-fixation primitive. Proxied services authenticate via bearer tokens or signed payloads, never cookies, so dropping `set-cookie` is purely defensive with no behavioral cost. Adds a regression test verifying both single- and multi-valued `Set-Cookie` headers (including the `getSetCookie` accessor) are removed while other headers pass through unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-019e525a-a5a8-75ca-98e4-8ce9433a3a52 --- .changeset/strip-upstream-set-cookie.md | 5 +++++ src/proxy/internal/Headers.test.ts | 18 ++++++++++++++++++ src/proxy/internal/Headers.ts | 15 ++++++++++++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/strip-upstream-set-cookie.md diff --git a/.changeset/strip-upstream-set-cookie.md b/.changeset/strip-upstream-set-cookie.md new file mode 100644 index 00000000..df8733a1 --- /dev/null +++ b/.changeset/strip-upstream-set-cookie.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Stripped `Set-Cookie` from upstream responses in `Proxy.scrubResponse` so an upstream service cannot set cookies under the proxy's origin. diff --git a/src/proxy/internal/Headers.test.ts b/src/proxy/internal/Headers.test.ts index ccacd5ed..c786782e 100644 --- a/src/proxy/internal/Headers.test.ts +++ b/src/proxy/internal/Headers.test.ts @@ -98,4 +98,22 @@ describe('scrubResponse', () => { expect(result.statusText).toBe('Created') expect(await result.text()).toBe('hello') }) + + // Regression: an upstream service must never be able to issue a cookie + // under the proxy's origin. Otherwise a compromised or attacker-influenced + // upstream can session-fixate (`Set-Cookie: session=evil; Domain=…`) every + // sibling subdomain of the proxy. See the docblock on `scrubResponse`. + test('behavior: strips set-cookie so upstream cannot set cookies on proxy origin', () => { + const response = new Response('body', { + headers: [ + ['Set-Cookie', '__Secure-session=evil; Domain=.example.com; Secure; HttpOnly'], + ['Set-Cookie', 'tracking=1; Path=/'], + ['Content-Type', 'application/json'], + ], + }) + const result = Headers.scrubResponse(response) + expect(result.headers.has('set-cookie')).toBe(false) + expect(result.headers.getSetCookie?.() ?? []).toEqual([]) + expect(result.headers.get('content-type')).toBe('application/json') + }) }) diff --git a/src/proxy/internal/Headers.ts b/src/proxy/internal/Headers.ts index 5ca1a274..8f864fbb 100644 --- a/src/proxy/internal/Headers.ts +++ b/src/proxy/internal/Headers.ts @@ -29,11 +29,24 @@ export function scrub(headers: Headers): Headers { return scrubbed } -/** Strips `content-encoding` and `content-length` from an upstream response so the proxy can re-stream it. */ +/** + * Strips re-streaming headers (`content-encoding`, `content-length`) and + * security-sensitive headers (`set-cookie`) from an upstream response. + * + * `set-cookie` is dropped because a paid API proxy must never let an upstream + * service set cookies in the user's browser under the proxy's origin. If a + * compromised, misbehaving, or attacker-influenced upstream returned + * `Set-Cookie: session=evil; Domain=.example.com`, the browser would honor it + * for every sibling subdomain of the proxy — turning any future path-confusion + * or open-redirect bug in the surrounding deployment into a session-fixation + * primitive. Proxied services authenticate via bearer tokens / signed + * payloads, never cookies, so dropping `set-cookie` is purely defensive. + */ export function scrubResponse(response: Response): Response { const headers = new Headers(response.headers) headers.delete('content-encoding') headers.delete('content-length') + headers.delete('set-cookie') return new Response(response.body, { status: response.status, statusText: response.statusText, From bed1021c7f7fc6b13bb881c7be906f2274b311f1 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Sat, 23 May 2026 11:25:25 +1000 Subject: [PATCH 2/2] chore(deps): bump qs override to 6.15.2 to clear audit advisory GHSA-q8mj-m7cp-5q26 covers qs >=6.11.1 <=6.15.1 (DoS in qs.stringify). The existing override pinned to 6.14.2, which now falls inside the vulnerable range. Bumps the override range to <=6.15.1 and pins to the patched 6.15.2. Unblocks 'Checks' job (pnpm audit) on this branch and main. Amp-Thread-ID: https://ampcode.com/threads/T-019e525a-a5a8-75ca-98e4-8ce9433a3a52 --- pnpm-lock.yaml | 14 +++++++------- pnpm-workspace.yaml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 558b7d2b..41ac7837 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ overrides: path-to-regexp@<8.4.0: 8.4.0 tar@<=7.5.10: 7.5.11 '@modelcontextprotocol/sdk@>=1.10.0 <=1.25.3': 1.26.0 - qs@>=6.7.0 <=6.14.1: 6.14.2 + qs@>=6.7.0 <=6.15.1: 6.15.2 ip-address@<=10.1.0: 10.1.1 minimatch@>=5.0.0 <5.1.8: 5.1.8 minimatch@>=9.0.0 <9.0.7: 9.0.7 @@ -3698,8 +3698,8 @@ packages: pure-rand@8.4.0: resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -6007,7 +6007,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.14.2 + qs: 6.15.2 raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: @@ -6517,7 +6517,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.14.2 + qs: 6.15.2 range-parser: 1.2.1 router: 2.2.0 send: 1.2.1 @@ -7313,7 +7313,7 @@ snapshots: pure-rand@8.4.0: {} - qs@6.14.2: + qs@6.15.2: dependencies: side-channel: 1.1.0 @@ -7655,7 +7655,7 @@ snapshots: stripe@17.7.0: dependencies: '@types/node': 25.8.0 - qs: 6.14.2 + qs: 6.15.2 strtok3@10.3.5: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7d8b3439..f425bda7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,7 +18,7 @@ overrides: path-to-regexp@<8.4.0: '8.4.0' tar@<=7.5.10: '7.5.11' '@modelcontextprotocol/sdk@>=1.10.0 <=1.25.3': '1.26.0' - qs@>=6.7.0 <=6.14.1: '6.14.2' + qs@>=6.7.0 <=6.15.1: '6.15.2' ip-address@<=10.1.0: '10.1.1' minimatch@>=5.0.0 <5.1.8: '5.1.8' minimatch@>=9.0.0 <9.0.7: '9.0.7'