-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Regex audience matching without anchors is a security footgun #1019
Description
Summary
When options.audience contains a RegExp, verify() calls audience.test(targetAudience) without enforcing that the regex is anchored. This means a developer who writes:
jwt.verify(token, secret, { audience: /api\.myapp\.com/ });will inadvertently accept tokens with audiences like evil-api.myapp.com.attacker.com — the . matches any character and there are no ^/$ anchors.
The string comparison path (audience === targetAudience) is strict. The regex path silently shifts the security burden to the caller.
Reproduction
const jwt = require('jsonwebtoken');
const secret = 'test-secret';
const token = jwt.sign({ aud: 'evil-api.myapp.com.attacker.com' }, secret);
// Developer intends to only accept "api.myapp.com"
jwt.verify(token, secret, { audience: /api\.myapp\.com/ }, (err, decoded) => {
console.log(err); // null — no error!
console.log(decoded); // token accepted despite malicious audience
});Impact
This is not a vulnerability in the library itself — the regex works as designed. But it's a footgun: developers who mix string and regex audience checks may not realize the security model differs between the two paths. The string path is exact-match. The regex path accepts partial matches unless the developer manually adds ^ and $.
Given that audience validation is a security-critical check, the gap between "looks like it works" and "actually secure" is a concern.
Suggestions (pick any)
- Document it prominently — Add a note to the README's audience section warning that regex audiences must be anchored to avoid partial matches.
- Warn on unanchored regexes — If the regex source doesn't start with
^or end with$, emit a console warning or throw. - Auto-anchor — Wrap unanchored regexes in
^(?:...)$before testing. This would be a breaking change for anyone relying on partial matches intentionally.
Option 1 is the lowest-friction fix. Option 2 provides defense-in-depth without breaking existing behavior.
Relevant code
return audiences.some(function (audience) {
return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience;
});