Skip to content

Regex audience matching without anchors is a security footgun #1019

@ekreloff

Description

@ekreloff

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)

  1. Document it prominently — Add a note to the README's audience section warning that regex audiences must be anchored to avoid partial matches.
  2. Warn on unanchored regexes — If the regex source doesn't start with ^ or end with $, emit a console warning or throw.
  3. 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

verify.js audience check:

return audiences.some(function (audience) {
  return audience instanceof RegExp ? audience.test(targetAudience) : audience === targetAudience;
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions