導入:改ざん不可能なはずのトークン
「JWTって署名が付いてるから改ざんされませんよね?」
認証基盤の設計レビューで、私はそう発言しました。チームの誰もが同意し、JWTベースの認証システムは予定通りリリースされました。
数週間後、脆弱性診断レポートの一番上に赤字の【Critical】が輝いていました。
「JWTヘッダーの alg を none に書き換えることで、署名検証がスキップされ、任意のユーザーとして認証が通ります。管理者権限の奪取が可能です」
署名で守られている「はず」のトークンが、ヘッダーのたった1フィールドの書き換えで無力化される。JWTを「なんとなく安全」と信じていた私の認識は、根本から間違っていました。
技術解説:JWTの構造と alg: none 攻撃
JWTの3つの構成要素
JWTは . で区切られた3つのパート(Header・Payload・Signature)で構成されます。
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiaG9nZSIsInJvbGUiOiJ1c2VyIn0.署名部分
│ │ │
├─ Header(Base64) ├─ Payload(Base64) ├─ Signature
│ {"alg":"HS256"} │ {"user":"hoge","role":"user"} │ HMAC-SHA256で生成
サーバーは、Signatureを検証して「Payloadが改ざんされていないこと」を確認します。
しかし、どのアルゴリズムで検証するかを、トークン自身のHeader(alg)に委ねているのが落とし穴です。
攻撃の手口
攻撃者は自分のトークンを以下のように書き換えます。
// Header を書き換え
{"alg": "none"} // ← 署名アルゴリズムを「なし」にする
// Payload を書き換え
{"user": "hoge", "role": "admin"} // ← 自分をadminに昇格
// Signature を空にする
脆弱なJWTライブラリは、トークンのHeaderに "alg": "none" と書かれていると、「あ、このトークンは署名不要なんだな」と解釈し、Signatureの検証を完全にスキップしてしまいます。
結果、Payloadの "role": "admin" がそのまま信頼され、一般ユーザーが管理者としてログインできてしまうのです。
なぜこんな設計になっているのか
JWT仕様(RFC 7519)自体が "alg": "none" を「Unsecured JWT」として定義しているため、仕様に忠実なライブラリほどこの挙動を許容してしまいます。
「仕様通りだから安全」という思い込みが、最大の脆弱性だったのです。
JWTを安全に扱うための鉄則
1. サーバー側でアルゴリズムをホワイトリスト指定する
トークンのHeaderに書かれた alg を信頼しないでください。
サーバー側のコードで「HS256とRS256のみ許可」と明示的に制限します。
// Node.js (jsonwebtoken) の例
jwt.verify(token, secret, { algorithms: ['HS256'] }); // ← ここが重要
2. 秘密鍵の管理を厳格にする
HS256(共通鍵)なら鍵の漏洩=全トークンの偽造が可能です。鍵のローテーション運用も設計に組み込みましょう。
3. トークンの失効(Revocation)戦略を持つ
JWTは「ステートレス」が売りですが、裏を返せば「一度発行したら有効期限まで無効化できない」という爆弾です。リフレッシュトークンとの併用やブラックリスト機構の検討が必須です。
「JWTは署名されているから安全」──この一文だけで思考を止めていた私は、仕組みを理解していないのと同じでした。 署名の「検証方法」をトークン自身が指定するという構造的リスクを知った上で、初めてJWTと正しく付き合えるのです。