Fix polynomial ReDoS in Bearer-token extraction (CodeQL js/polynomial-redos)

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.
This commit is contained in:
Adam Moussa 2026-06-26 13:06:27 -04:00
parent 00762de0a6
commit f4111e4e17
2 changed files with 15 additions and 1 deletions

View file

@ -260,4 +260,14 @@ describe('extractBearerToken', () => {
it('throws AuthError on a non-Bearer header', () => { it('throws AuthError on a non-Bearer header', () => {
expect(() => extractBearerToken('Basic abc')).toThrow(AuthError); 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);
});
}); });

View file

@ -189,7 +189,11 @@ export function extractBearerToken(req: unknown): string {
if (!header) { if (!header) {
throw new AuthError('missing_token', 'No Authorization header present.'); 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(); const token = match?.[1]?.trim();
if (!token) { if (!token) {
throw new AuthError('missing_token', 'Authorization header is not a Bearer token.'); throw new AuthError('missing_token', 'Authorization header is not a Bearer token.');