ReDoS in verify() when audience option is a RegExp: attacker-controlled aud claim → catastrophic backtracking
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- javascript, node.js
- Domain
- authentication, backend, security
Research direction
Start in verify.js around the audience-check loop and run the supplied reproduction and fast-check property to observe the delay. Review the related audience option documentation in README and verify.d.ts. Done means the audience RegExp path has a bounded, documented behavior for attacker-controlled claims and the regression scenario is covered.
Written by the indexing model from the issue text.
Description
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
audbefore 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+
- Dominant language
- JavaScript
- Stars
- 18.2k
- Forks
- 1.3k
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from auth0/node-jsonwebtoken
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
auth0/node-jsonwebtoken#1042 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
auth0/node-jsonwebtoken#1000 · 2 comments · 1 reaction ·
-
Difficulty 4/5 3-5 days Newbie friendliness 65/100
auth0/node-jsonwebtoken#1046 ·
-
Difficulty 5/5 Over a week Newbie friendliness 10/100
auth0/node-jsonwebtoken#1034 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 48/100
auth0/node-jsonwebtoken#1032 ·
All issues in auth0/node-jsonwebtoken
Similar issues
-
Bug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Automattic/safe-publish#594 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
dream-num/dsh-univer-office#104 ·
-
comp/dashboard invalid P3
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
NousResearch/hermes-agent#121143 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
avniproject/avni-webapp#1811 ·
-
area/auroraboot area/webui bug
Difficulty 2/5 1-3 hours Newbie friendliness 75/100