From 9ecb11a4d1be164ddcc5ccbe45c58b1e3132eda5 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Wed, 29 Jul 2026 21:43:46 -0400 Subject: [PATCH] fix(express): encode the provider id on the oauth provider admin routes PATCH and DELETE /system-config/oauth-providers/:id interpolated req.params.id straight into the upstream URL. Every other proxied route param goes through encodeURIComponent, and these two were missed when that pass landed. An id of `abc?admin=1` was forwarded as /system-config/oauth-providers/abc?admin=1, turning attacker-controlled input into an upstream query parameter. A param carrying an encoded `/` could reshape the upstream path the same way. Found while porting the route table to a second adapter: the other 17 interpolations encode, these 2 did not. The routes require an authenticated access session, so this is not reachable anonymously. --- .changeset/encode-oauth-provider-id.md | 11 ++++++ packages/express/src/createServer.ts | 6 ++-- .../tests/proxyQueryForwarding.test.js | 36 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 .changeset/encode-oauth-provider-id.md diff --git a/.changeset/encode-oauth-provider-id.md b/.changeset/encode-oauth-provider-id.md new file mode 100644 index 0000000..fcf94c7 --- /dev/null +++ b/.changeset/encode-oauth-provider-id.md @@ -0,0 +1,11 @@ +--- +"@seamless-auth/express": patch +--- + +Encode the provider id on the OAuth provider admin routes before forwarding it upstream. + +`PATCH` and `DELETE /system-config/oauth-providers/:id` interpolated `req.params.id` straight into the upstream URL. Every other proxied route param goes through `encodeURIComponent`, and these two were missed when that pass landed. A param carrying `?`, `#`, or an encoded `/` was decoded into the URL raw, so it could append or override upstream query parameters or reshape the upstream path. + +An id of `abc?admin=1` was forwarded as `/system-config/oauth-providers/abc?admin=1`, turning attacker-controlled input into an upstream query parameter. It is now forwarded as a single encoded path segment, which upstream rejects as an unknown id. + +The routes require an authenticated access session, so this is not reachable anonymously. diff --git a/packages/express/src/createServer.ts b/packages/express/src/createServer.ts index 28cf042..5228c69 100644 --- a/packages/express/src/createServer.ts +++ b/packages/express/src/createServer.ts @@ -495,7 +495,8 @@ export function createSeamlessAuthServer( r.patch( "/system-config/oauth-providers/:id", proxyWithIdentity( - (req) => `system-config/oauth-providers/${req.params.id}`, + (req) => + `system-config/oauth-providers/${encodeURIComponent(routeParam(req, "id"))}`, "access", "PATCH", ), @@ -503,7 +504,8 @@ export function createSeamlessAuthServer( r.delete( "/system-config/oauth-providers/:id", proxyWithIdentity( - (req) => `system-config/oauth-providers/${req.params.id}`, + (req) => + `system-config/oauth-providers/${encodeURIComponent(routeParam(req, "id"))}`, "access", "DELETE", ), diff --git a/packages/express/tests/proxyQueryForwarding.test.js b/packages/express/tests/proxyQueryForwarding.test.js index d6332fc..7896624 100644 --- a/packages/express/tests/proxyQueryForwarding.test.js +++ b/packages/express/tests/proxyQueryForwarding.test.js @@ -97,3 +97,39 @@ describe("proxy query forwarding", () => { ); }); }); + +// A route param interpolated raw into the upstream URL can append query +// parameters or reshape the path, which is what #65 fixed elsewhere. These two +// oauth-provider routes were missed by that pass. +describe("route params stay in one upstream path segment", () => { + const originalFetch = global.fetch; + let requestedUrl; + + beforeEach(() => { + requestedUrl = undefined; + global.fetch = jest.fn(async (url) => { + requestedUrl = String(url); + return createJsonResponse(200, { ok: true }); + }); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it.each([ + ["patch", "abc?admin=1"], + ["delete", "abc?admin=1"], + ["patch", "abc#frag"], + ["patch", "../../admin/users"], + ])("%s /system-config/oauth-providers/:id encodes %s", async (method, id) => { + await request(createApp()) + [method](`/auth/system-config/oauth-providers/${encodeURIComponent(id)}`) + .set("Cookie", createAccessCookie()) + .send({}); + + expect(requestedUrl).toBe( + `https://auth.example.com/system-config/oauth-providers/${encodeURIComponent(id)}`, + ); + }); +});