ReDoS in verify() when audience option is a RegExp: attacker-controlled aud claim → catastrophic backtracking
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 静か
- 技術スタック
- javascript, node.js
調査の方向性
verify.js の audience-check ループ付近から始め、提供された再現手順と fast-check property を実行して遅延を観察します。README と verify.d.ts にある関連する audience オプションのドキュメントを確認します。完了条件は、audience RegExp パスが攻撃者に制御された claims に対して、制限され文書化された動作を持ち、回帰シナリオがカバーされていることです。
索引モデルが issue の本文から書いたものです。
説明
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+
- 主要言語
- JavaScript
- スター
- 18.2k
- フォーク
- 1.3k
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
auth0/node-jsonwebtoken のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
auth0/node-jsonwebtoken#1042 · コメント 1 件 ·
-
`jwt.sign()` callback is executed twice for "The payload already has an "..." property" errors対応中かも @cobyfrombrooklyn-bot が 229 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
auth0/node-jsonwebtoken#1000 · コメント 2 件 · リアクション 1 件 ·
-
難易度 4/5 3〜5日 初心者へのやさしさ 45/100
auth0/node-jsonwebtoken#1048 ·
-
verify() resolves a string secret by attempting createPublicKey() first, costing 4x-52x on the HS* path対応中かも @Hashim1999164 が 23 日前に担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 65/100
auth0/node-jsonwebtoken#1046 ·
-
難易度 5/5 1週間以上 初心者へのやさしさ 10/100
auth0/node-jsonwebtoken#1034 ·
auth0/node-jsonwebtoken の issue をすべて見る
似ている issue
-
channels:add check:passed
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
メンテナーはふだん 9 日以内に返信
-
documentation
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
githubnext/gh-aw-workshop#4496 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
swc-project/swc#12516 ·
メンテナーはふだん 1 日以内に返信
-
Opening Files on a demo drive logs you out対応中かも @RChang06 が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
メンテナーはふだん 2 日以内に返信
-
feature-request
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
omegaup/omegaup#10323 · コメント 1 件 ·
メンテナーはふだん 5 日以内に返信