PyJWT: 非対称PEMの検出回避:空白や改行コードが変更された公開鍵は、HS/非対称の区別を行う保護機能を回避する。
**前提条件**(以下の両方の条件を満たす必要があります。両者はデプロイメント時の設定であり、リクエスト時に攻撃者によって制御されるものではありません):
- `jwt.decode` の許可リストが、HMACアルゴリズムと非対称アルゴリズムを混在させている場合、例えば `algorithms=["ES256", "HS256"]` (RFC 8725 の脆弱性に対する保護機能)。
- 検証キーが、`PyJWK` 以外のパスで、生のPEMテキスト/バイトとして渡される。このバイト形式は、`cryptography` のローダーが受け入れるものの、PyJWT の `is_pem_format` 正規表現では認識されない(PEMの開始/終了マーカーの近くの空白/インデント、CRのみの行末、または単一行に折りたたまれたPEM)。このような形式は、インデントされたYAML/JSONブロック、単一行の環境変数またはJSON文字列、または設定ツールを通じたCR/LFの変換によって自然に発生する。
攻撃者は、定義上公開されている公開検証キーと、`cryptography` がインストールされている必要がある。
CVE-2022-29217 の修正は、HMACアルゴリズムに渡された非対称キーを拒否するが、これは `is_pem_format()` がキーをPEMとして認識する場合にのみ適用される。この認識機能は、後でキーを解析するローダーが受け入れるバイト形式よりも厳格な形式のみを受け入れるため、保護機能が認識できないPEMは、有効な公開鍵としてロードされ、その後、HMACのシークレットとして使用される。これは、CVE-2022-29217 ファミリーの保護機能の不完全な回避である。
### 概要
PEM公開鍵において、マーカーの近くの空白、CRのみの行末、または単一行に折りたたまれた場合、PyJWTの `is_pem_format()` は `False` を返す一方、`cryptography.load_pem_public_key()` は同じバイトを受け入れる。`HMACAlgorithm.prepare_key` における非対称キーの拒否が回避され、公開鍵がHMACのシークレットとなり、公開鍵を知っている攻撃者は、検証許可リストがHMACと非対称アルゴリズムを混在させている場合に、有効な `HS256` トークンを偽造することができる(普遍的な偽造)。
### 詳細
`jwt/algorithms.py` の 331 行から 335 行にある `HMACAlgorithm.prepare_key` 関数には、タイプ不一致を検出する唯一の箇所が含まれています。
```python
if is_pem_format(key_bytes) or is_ssh_key(key_bytes):
raise InvalidKeyError(
"指定されたキーは非対称キーまたは X.509 証明書であり、"
"HMAC のシークレットとして使用すべきではありません。"
)
```
上記の条件のいずれも満たさない場合、357 行で `key_bytes` が変更されずに返されます。つまり、PEM テキストが直接 HMAC のシークレットとして使用されます。
`is_pem_format` 関数 (`jwt/utils.py` の 116 行から 127 行) は `bool(_PEM_RE.search(key))` で定義されており、`_PEM_RE` は `----[- ]BEGIN (...)[- ]----\r?\n`、次に `.+?\r?\n`、そして最後に END マーカーを必要とします。各 `\r?\n` の LF (改行) は必須であり、マーカーは改行の直後に配置され、マーカーの隣には `[- ]` のみ許容されます。ランダムな空白は許可されません。したがって、END マーカーの前にタブまたはスペースがある場合、または `\r` のみで終了する場合、または 1 行に折り返されている場合、PEM として認識されません。`cryptography` ライブラリの `load_pem_public_key` 関数は、これらの形式を許容し、キーを返します。
影響範囲: `jwt/api_jws.py` の 386 行で、許可リストのチェックが行われ (`HS256` がリストに含まれている場合に成功)、392 行で `alg_obj.prepare_key(key)` が呼び出されます。上記のタイプ不一致のチェックは、許可リストと公開鍵を HMAC シークレットとして使用する間の唯一の障壁です。
### 脆弱性の検証
脆弱なパス: `jwt/algorithms.py` の 331 行 ( `is_pem_format` によって制御) -> `is_pem_format` が、ローダーによって受け入れられる PEM に対して `False` を返す -> `jwt/algorithms.py` の 357 行で、公開鍵のバイトが HMAC シークレットとして返される -> `HS256` の検証が成功する。
PyJWT 2.13.0 (コミット `7144e453`) で、`cryptography` 49.0.0 を使用し、公開 API のみを使用して再現しました。EC (ES256) と RSA-2048 (RS256) の鍵それぞれについて、正しい公開鍵 PEM から開始し、変更を加え、`is_pem_format` が `False` を返すのを確認し(ただし `cryptography` は依然としてバイトを読み込む)、次に、公開鍵テキストを HMAC シークレットとして使用して、`alg=HS256` で署名されたトークンを検証します。検証対象のアルゴリズムは `algorithms=["ES256","HS256"]` (または `["RS256","HS256"]`) です。
観察された出力:
pyjwt 2.13.0
ec 標準 (コントロール) is_pem_format=True crypto_loads=True forgery=BLOCKED:InvalidKeyError
ec マーカーに隣接 (END の前のタブ) is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)
ec CR のみを使用した終端符 is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)
ec 折り返された単一行 is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)
rsa 標準 (コントロール) is_pem_format=True crypto_loads=True forgery=BLOCKED:InvalidKeyError
rsa マーカーに隣接 (END の前のタブ) is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)
rsa CR のみを使用した終端符 is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)
rsa 折り返された単一行 is_pem_format=False crypto_loads=True forgery=FORGED(superadmin)
[コントロール B] 単一のアルゴリズム [ES256] の許可リストと偽造された HS256: REJECTED:InvalidAlgorithmError
結果: ALL-INVARIANTS-HOLD
両方の鍵タイプにおいて、変更を加えたすべての形式で、`superadmin` として受け入れられるトークンが生成されました。コントロール:変更されていない PEM は、正しく `InvalidKeyError` で拒否されます(この保護機能は機能しており、変更は有効です)。単一のアルゴリズムの許可リスト `["ES256"]` は、偽造された `HS256` トークンを `InvalidAlgorithmError` で拒否します(混合された許可リストは、必要な前提条件です)。
再現手順:
1. EC P-256(またはRSA-2048)の鍵ペアを生成し、公開鍵をPEM形式でシリアライズします。
2. PEMデータを、ローダーが受け入れ可能で、正規表現では検出されないように変更します。例えば、`-----END`の前にタブを挿入したり、終端文字を単純な`\r`に変換したり、すべての行を1つに結合したりします。
3. `jwt.utils.is_pem_format(変更されたデータ)がFalse`であり、`cryptography.hazmat.primitives.serialization.load_pem_public_key(変更されたデータ)`が成功することを確認します。
4. `jwt.encode({"sub":"superadmin"}, 変更されたデータ, algorithm="HS256")`を実行し、その後`jwt.decode(トークン, 変更されたデータ, algorithms=["ES256","HS256"])`を実行します。これにより、検証が成功します。
### 影響
暗号学的署名検証の回避(CWE-347):不完全な非対称鍵保護により、アルゴリズムの混同が再び有効になります。攻撃者が公開検証鍵のみを知っている場合、上記の2つの展開条件を満たすと、任意のクレームを持つトークンを生成し、それが正当なものとして検証されます。`PyJWK`の検証パスは、単一のアルゴリズムに制限されており、影響を受けません。`enforce_minimum_key_length`(デフォルトでは無効)は、2048ビット/P-256のPEMをブロックしません。上記の条件が満たされた場合、影響は重大(完全な偽造)です。複合条件は現実的ですが、特定の実際の展開環境では観察されていません。そのため、この脆弱性は「重大」と評価され、展開条件はCVSSに記録されています。
## メンテナからの更新 — 2026年9月10日
PyJWT 2.13.0で報告された非対称鍵保護の回避を再現しました。ローダーが受け入れ可能な形式の変更が加えられたPEM公開鍵が、`is_pem_format`によって検出されず、その結果、検証呼び出し時にHMACシークレットとして受け入れられました。標準的なPEM形式は引き続きブロックされており、単一のアルゴリズムを許可するリストでは、偽造されたHS256トークンが拒否されました。
修正は `8b4e233a22206b34ec1186e912e75c0b2396ac07` としてコミットされました。PyJWT は、サポートされている PEM の BEGIN/END マーカーを 1 回の処理でスキャンし、対応するラベルを保持し、正規表現のバックトラックなしで、重なり合うマーカーを処理します。回帰テストの範囲には、RSA ローダーが受け入れる変更、不完全な繰り返しマーカー、後続の有効な PEM ブロック、および重なり合う END/BEGIN マーカーが含まれます。この修正は、DER キーの分類を広げることも、呼び出し元のアルゴリズムの許可リストポリシーを変更することもありません。
署名されたコミットに対する検証テストは、400 件のテストで合格し、意図的に暗号化環境に関する 4 件のテストをスキップしました。Ruff のフォーマット/Lint および Python 3.9 の mypy tox ターゲットも合格しました。独立した Astra/max のレビューで、最終的なスナップショットが受け入れられ、O(n) のスキャン、制限されたストレージ、およびブロックする互換性やセキュリティ上の問題がないことが確認されました。この修正はまだリリースされていません。この脆弱性に関する情報は、CVSS 3.1 スコア 9.1 および CWE-347 で「重大」として残っています。
## メンテナからの更新 — 2026年9月11日
この脆弱性に対する検証済みの修正は、PyJWT 2.14.0 に含まれており、2026年9月11日にリリースされ、PyPI で利用可能です。PyJWT 2.14.0 は、この修正を含む最初のリリースです。この脆弱性に関する情報は、2.14.0 が修正されたバージョンとして記録され、公開されています。
影響を受けるパッケージ:
- pip (Python) PyJWT: 影響を受けるバージョンは 2.13.0 以下、修正は 2.14.0 で行われました。