Description
jwt.verify(token, secret, { audience: regex }) passes the attacker-controlled aud claim of the JWT directly into RegExp.test(). If the application uses a regex with vulnerable patterns (nested quantifiers, ambiguous alternation), the verification call hangs for seconds-to-minutes per request — classic ReDoS via the audience claim.
Reproduction (jsonwebtoken 9.0.2)
const jwt = require('jsonwebtoken');
const SECRET = 'test-secret';
// Application uses RegExp for audience matching (documented feature)
const audRegex = /(a+)+$/;
// Attacker crafts a token with a malicious `aud` claim and signs it
// (in real scenarios the attacker controls some path that signs user-supplied audiences)
const token = jwt.sign({ aud: 'a'.repeat(25) + '!' }, SECRET);
const t0 = Date.now();
try { jwt.verify(token, SECRET, { audience: audRegex }); } catch (_) {}
console.log('verify took', Date.now() - t0, 'ms');
// Output: ~3000ms with 25 a's; ~12s with 27; ~93s with 30.
Property that fails
import fc from "fast-check";
import jwt from "jsonwebtoken";
const SECRET = 's';
const audRegex = /(a+)+$/;
fc.assert(fc.property(
fc.integer({min: 5, max: 30}),
(n) => {
const tok = jwt.sign({ aud: 'a'.repeat(n) + '!' }, SECRET);
const t0 = Date.now();
try { jwt.verify(tok, SECRET, { audience: audRegex }); } catch (_) {}
return Date.now() - t0 < 500; // < 500 ms for any small input
}
));
// Shrinks to n=22 ~ 25
Threat model
Many real applications use RegExp for audience matching (multi-tenant subdomains: /^https:\/\/[^.]+\.example\.com$/, wildcard tenants, microservice families). Where the aud claim originates from anything other than a hard-coded list — for example, a federated token-exchange endpoint where one party signs a token containing the next service's name — an attacker who controls that audience string can supply a payload that exhausts a CPU core per call.
Concrete impact: a single ~80-byte JWT request blocks an event-loop thread for ≥10 seconds. A few requests/second saturate the server.
This is the same class as CVE-2024-21534 (jsonwebtoken older), CVE-2024-21501 (sanitize-html), CVE-2026-35041 (fast-jwt's identical issue with allowedAud).
Root cause
verify.js, audience-check loop (around lines 194-207):
const match = target.some((targetAudience) => {
return audiences.some((audience) => {
return audience instanceof RegExp
? audience.test(targetAudience) // <- no length cap, no timeout
: audience === targetAudience;
});
});
The library:
- Doesn't limit the length of
aud before RegExp.test.
- Doesn't surface the ReDoS risk in
verify's docs for the RegExp-audience path.
- Trusts the application to have audited its audience regex — but the attacker, not the application, supplies the input to that regex.
Suggested fix
In verify.js, before calling audience.test(targetAudience), enforce a configurable max-length on targetAudience (e.g. 256 chars by default). Throw JsonWebTokenError('audience claim exceeds length limit') on overflow.
Additionally, document the ReDoS hazard for the regex-audience path in README and verify.d.ts, and recommend re2 or static-regex-safety checking for application-supplied audience regexes.
Environment
- jsonwebtoken: 9.0.2 (latest on npm)
- Node: 20+
Description
jwt.verify(token, secret, { audience: regex })passes the attacker-controlledaudclaim of the JWT directly intoRegExp.test(). If the application uses a regex with vulnerable patterns (nested quantifiers, ambiguous alternation), the verification call hangs for seconds-to-minutes per request — classic ReDoS via the audience claim.Reproduction (jsonwebtoken 9.0.2)
Property that fails
Threat model
Many real applications use RegExp for audience matching (multi-tenant subdomains:
/^https:\/\/[^.]+\.example\.com$/, wildcard tenants, microservice families). Where theaudclaim originates from anything other than a hard-coded list — for example, a federated token-exchange endpoint where one party signs a token containing the next service's name — an attacker who controls that audience string can supply a payload that exhausts a CPU core per call.Concrete impact: a single ~80-byte JWT request blocks an event-loop thread for ≥10 seconds. A few requests/second saturate the server.
This is the same class as CVE-2024-21534 (jsonwebtoken older), CVE-2024-21501 (
sanitize-html), CVE-2026-35041 (fast-jwt's identical issue withallowedAud).Root cause
verify.js, audience-check loop (around lines 194-207):The library:
audbefore RegExp.test.verify's docs for the RegExp-audience path.Suggested fix
In
verify.js, before callingaudience.test(targetAudience), enforce a configurable max-length ontargetAudience(e.g. 256 chars by default). ThrowJsonWebTokenError('audience claim exceeds length limit')on overflow.Additionally, document the ReDoS hazard for the regex-audience path in README and
verify.d.ts, and recommendre2or static-regex-safety checking for application-supplied audience regexes.Environment