From f4111e4e172b78758c26f38fa97ddac15cb6c4f1 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Fri, 26 Jun 2026 13:06:27 -0400 Subject: [PATCH] Fix polynomial ReDoS in Bearer-token extraction (CodeQL js/polynomial-redos) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractBearerToken matched /^Bearer\s+(.+)$/ — \s and . both match a space, so the two quantifiers overlap and a crafted header can drive polynomial backtracking. Require the capture to start with a non-whitespace char (/^Bearer\s+(\S.*)$/), removing the ambiguity → linear match. Behavior is unchanged for real tokens; +2 regression tests. --- packages/shared/src/cognito-auth.test.ts | 10 ++++++++++ packages/shared/src/cognito-auth.ts | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/cognito-auth.test.ts b/packages/shared/src/cognito-auth.test.ts index 9a2889c..15fa654 100644 --- a/packages/shared/src/cognito-auth.test.ts +++ b/packages/shared/src/cognito-auth.test.ts @@ -260,4 +260,14 @@ describe('extractBearerToken', () => { it('throws AuthError on a non-Bearer header', () => { expect(() => extractBearerToken('Basic abc')).toThrow(AuthError); }); + + // ReDoS guard (CodeQL js/polynomial-redos): the matcher is /\s+(\S.*)/, not + // the ambiguous /\s+(.+)/. These pin the behavior that the `\S` fix preserves. + it('still captures a token that follows multiple separating spaces', () => { + expect(extractBearerToken('Bearer abc.def')).toBe('abc.def'); + }); + + it('rejects a "Bearer" header with no token after the whitespace', () => { + expect(() => extractBearerToken(`Bearer ${' '.repeat(5_000)}`)).toThrow(AuthError); + }); }); diff --git a/packages/shared/src/cognito-auth.ts b/packages/shared/src/cognito-auth.ts index a4b0c5c..292c9e9 100644 --- a/packages/shared/src/cognito-auth.ts +++ b/packages/shared/src/cognito-auth.ts @@ -189,7 +189,11 @@ export function extractBearerToken(req: unknown): string { if (!header) { throw new AuthError('missing_token', 'No Authorization header present.'); } - const match = /^Bearer\s+(.+)$/i.exec(header.trim()); + // `\s+(\S.*)` — NOT `\s+(.+)`: requiring the capture to start with a + // non-whitespace char removes the quantifier overlap (both `\s` and `.` + // match a space), which otherwise allows polynomial backtracking on a + // crafted all-whitespace header (CodeQL js/polynomial-redos). Linear now. + const match = /^Bearer\s+(\S.*)$/i.exec(header.trim()); const token = match?.[1]?.trim(); if (!token) { throw new AuthError('missing_token', 'Authorization header is not a Bearer token.');