PyJWT BOM バイパス

## 影響を受けるパッケージ

- **パッケージ**: PyJWT (`pyjwt` on PyPI)
- **リポジトリ**: https://www.google.com/url?q=https://github.com/jpadilla/pyjwt&source=gmail&ust=1781794518474000&sa=E
- **影響を受けるバージョン**: 2.13.0
- **脆弱性の種類**: アルゴリズムの混同 / パッチの回避

---

## 根本原因

PyJWT 2.13.0では、`HMACAlgorithm.prepare_key()`(ファイル`jwt/algorithms.py`、おおよそ344行目)に、RSA公開鍵の情報をHMACシークレットとして使用することを防ぐためのガードが導入されました。これがCVE-2026-48526の根本原因です。

このガードは、`startswith(b"{")`を呼び出す前に`bytes.lstrip()`を使用しています。

```python
stripped = key_bytes.lstrip() # ASCII空白文字のみを削除
if stripped.startswith(b"{"): # JWKの検出
...
raise InvalidKeyError("指定されたキーは非対称鍵です...")
```

引数なしで`bytes.lstrip()`を使用すると、ASCII空白文字セット(`\x20 \t \n \r \x0b \x0c`)に含まれるバイトのみが削除されます。UTF-8 BOMプレフィックス(`\xef\xbb\xbf`)は削除されないため、BOMプレフィックスが付いたJWK JSONの場合、`stripped.startswith(b"{")`は`False`を返します。JWK検出ブロックが実行されず、RSA公開鍵のバイトがHMAC-SHA256シークレットとして静かに受け入れられます。

---

## PoCの概要(擬似コード — 攻撃に利用できるペイロードではありません)

```python
# 1. 攻撃者は、RSA公開鍵のJWK(例:/jwks.jsonエンドポイントから取得)を入手し、JSONの開始括弧の前にUTF-8 BOMバイトシーケンスを付加します。
# 2. 攻撃者は、BOMプレフィックスが付いたJWKをシークレットとして使用して、HS256でJWTに署名します。
# 3. 攻撃者は、以下の条件を満たす検証システムに偽造されたトークンを送信します。
# - 許可されるアルゴリズム:["HS256", "RS256"]
# - 同じRSA公開鍵をそのままのバイト列(BOMプレフィックス付きの鍵ファイル)として保持している
# 4. PyJWT 2.13.0は、BOMによってJWK検出チェックがスキップされるため、トークンを受け入れます。これにより、RSA JWKバイトが有効なHMACキーとして扱われます。
# 結果:検証システムによって、任意のクレーム(ロール、サブジェクトなど)が受け入れられます。
```

---

## 影響

認証されていない攻撃者が、対象となるアプリケーションのRSA公開鍵(これは設計上公開されており、JWKSエンドポイントや証明書から入手可能です)を知っている場合、任意のクレームを含むJWTトークンを偽造し、`algorithms=["HS256", "RS256"]`または同等の設定で構成されたPyJWT 2.13.0の検証器によって受け入れられる可能性があります。これにより、認証と認可の完全な回避(C:H/I:H)が発生します。攻撃の複雑度は高い(AC:H)と考えられます。なぜなら、攻撃者はRSA公開鍵を入手する必要があり、検証器は混合アルゴリズムの設定を使用している必要があるからです。認証は不要です(PR:N)。これはパッチの回避策であり、CVE-2026-48526を修正するために2.13.0にアップグレードしたユーザーも依然として脆弱です。

---

## 推奨される修正方法

**オプションA(最小限):** `lstrip()`を、JSON検出チェックの前に、既知のBOMプレフィックスを明示的に削除するように置き換えます。

```python
# ASCII空白文字に加えて、一般的なBOMプレフィックスを削除します
BOM_PREFIXES = (b"\xef\xbb\xbf", b"\xff\xfe", b"\xfe\xff")
stripped = key_bytes
for bom in BOM_PREFIXES:
if stripped.startswith(bom):
stripped = stripped[len(bom):]
break
stripped = stripped.lstrip()
```

**オプションB(より堅牢):** バイトプレフィックスのチェックではなく、`json.loads()`を検出メカニズムとして使用します。これにより、エンコーディングのバリエーションや空白文字がJSONパーサーによって処理されます。

```python
try:
test_obj = json.loads(key_bytes.strip())
if isinstance(test_obj, dict) and "kty" in test_obj:
raise InvalidKeyError("指定されたキーは非対称キーです...")
except (ValueError, UnicodeDecodeError):
pass
```

オプションBは、将来的なエンコーディングのバリエーションにも対応できるため、推奨されます。

---

## メンテナからの更新 — 2026年9月9日

PyJWT 2.13.0において、報告されているアルゴリズムの混乱を引き起こす脆弱性を再現しました。アプリケーションが生のRSA JWKをキーとして使用し、非対称アルゴリズムとHMACアルゴリズムの両方を許可している場合、既知の公開JWKのバイトで署名された偽のHS256トークンが、PythonのJSONデコーダーが受け入れ可能なエンコーディングでJWKが表現されている場合に受け入れられてしまうことがあります。通常の生のJWK、非対称アルゴリズムのみ、およびアルゴリズムに制限された`PyJWK`の設定では、このトークンは拒否されます。これはアプリケーションの設定条件ですが、回避策はPyJWT自体の生のJWKの検証機能にあり、PyJWTのセキュリティポリシーの範囲に含まれます。

修正は、`180783930de91876bc0d601f826a1f2956057291`としてコミットされました。`HMACAlgorithm.prepare_key()`は、解析されたJSONオブジェクトに対して、許容されるUTF-8/16/32エンコーディングにおける`kty`をチェックし、JWK以外のHMACキーのバイトを保持し、再帰ガードに到達した場合でも、深くネストされたJSONオブジェクトを保守的に拒否します。回帰テストには、BOMおよびBOMなしのエンコーディング、深い非JWKキー、深いJWKオブジェクト、およびペアになっていないサロゲート文字のケースが含まれています。

完全なテストスイートは、391件のテストと、意図的に暗号化環境をスキップした4件のテストで合格しました。独立したAstra/maxによるレビューで、コミット`180783930de91876bc0d601f826a1f2956057291`が受け入れられました。このレビューでは、エンコーディング/BOMの処理、再帰とサロゲート文字の動作、非オブジェクトキーの互換性の保持、および報告されたテスト結果が独立して確認されました。

この修正はまだリリースされていません。この脆弱性に関する注意喚起は、既存のCVSS 3.1スコア7.4で「高」のままです。修正されたバージョンは、リリース計画が確定するまで未定です。

## 分類更新 — 2026年9月10日

注意喚起の分類レビューを完了しました。提案されたCVSS 3.1ベクトルは`CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N`であり、提案されたCWE分類はCWE-347です。これらの分類は、文書化された影響を反映しており、影響範囲、修正状況、またはライフサイクル状態を変更するものではありません。

## メンテナ更新 — 2026年9月11日

このセキュリティアドバイザリに対する修正は、2026年9月11日にリリースされたPyJWT 2.14.0に含まれています。PyJWT 2.14.0は、この修正を含む最初のリリースです。このアドバイザリは、修正されたバージョンとして2.14.0が記録された状態で公開されています。

影響を受けるパッケージ:
- pip (Python) PyJWT: 影響を受けるバージョン: 2.13.0、修正されたバージョン: 2.14.0