【入門編】 JWTの脆弱性:Noneアルゴリズム攻撃と対策 – Web APIアーキテクチャ・データ連携実践ガイド

こんにちは!技術メディア主筆のネットワークスペシャリストです。

Web APIの世界へようこそ!モダンなWebサービスやスマホアプリを開発していると、必ずと言っていいほど耳にするのがREST APIとJWT(JSON Web Token:ジョットと読みます)ですよね。

REST APIは「ステートレス(サーバー側に前回の状態を保存しない)」という美しい設計原則を持っています。そのステートレスな世界で「私は正規のユーザーですよ!」と証明するためのデジタルな身分証明書(通行手形)として大活躍するのがJWTです。

しかし、この便利なJWTには、実装を少しでも油断すると「誰でも管理者になりすませてしまう」という背筋が凍るような歴史的脆弱性が存在します。それが今回解説する「Noneアルゴリズム攻撃」です。

今回は、インフラやWebセキュリティに初めて触れる方に向けて、現実世界の「郵便配達や手紙の封蝋(シーリングワックス)」に例えながら、パケットの裏側で起きているドラマと確実な防御策を一歩ずつ紐解いていきましょう!

—

1. まずはおさらい:JWT(通行手形)の仕組みとは?

JWTを理解するために、まずは現実世界の「公証人のサインが入った重要書類」をイメージしてみてください。

JWTは、ドット(.)で区切られた3つの部分からできています。

【ヘッダー】 . 【ペイロード】 . 【署名(シグネチャ)】

ブラウザやスマホアプリは、APIを呼び出す際にこの文字列をHTTPの Authorization: Bearer <JWT> ヘッダーに乗せてサーバーへ送ります。それぞれの役割を現実世界に例えてみましょう。

1. ヘッダー(Header)

  • 例え: 書類の表紙や「検印の押し方マニュアル」
  • 「この書類のサインは、どの印鑑(暗号化方式)を使って確かめてね」というメタ情報が書かれています。

2. ペイロード(Payload)

  • 例え: 書類の中身そのもの
  • 「ユーザーID: 12345」「ユーザー名: アリス」「権限: 一般ユーザー」といった実際のデータが書かれています。

3. 署名(Signature)

  • 例え: 偽造防止の「公証人のサイン」や「シーリングスタンプ(封蝋)」
  • サーバーだけが知っている秘密の鍵を使って、ヘッダーとペイロードを混ぜ合わせて計算したハッシュ値です。
[ クライアント ] 
       |
       |  HTTPリクエスト (JWT付き)
       v
[ Web APIサーバー ]
  1. ヘッダーを見る(「ふむふむ、HMAC-SHA256でサインしたんだな」)
  2. 自分の秘密鍵を使って、中身が改ざんされていないか「署名」を再計算して検証
  3. 合致すれば「OK!あなたはアリスさんですね」とアクセスを許可

このように、サーバーはデータベースへ毎回「この人誰だっけ?」と問い合わせに行かなくても、手元の秘密鍵で署名を検証するだけで、瞬時に正しいユーザーだと信頼できるわけですね。これがステートレスの美しさです。

—

2. 恐怖の「Noneアルゴリズム攻撃」のからくり

では、本題の「Noneアルゴリズム攻撃」とは一体何なのでしょうか?

仕様上の「署名なし(none)」という落とし穴

実は、JWTの標準規格(RFC 7519やRFC 7518)には、通信のデバッグや、すでに安全が保証された閉域網などで使うために「署名を必要としないモード(alg: none)」が定義されています。

現実世界で言えば、書類の表紙に「検証方法:不要(サインなしでそのまま通してください)」と書くことがルール上許されているようなものです。

攻撃の流れ:自分で勝手に「サイン不要」にする

悪意のある攻撃者はこう考えました。

> 「ペイロードの中身を role: admin(管理者)に書き換えたいけれど、秘密鍵を知らないから正しい署名が作れないな……。
> あ、そうだ!ヘッダーのアルゴリズム指定を none に書き換えて、署名欄を空っぽにして送れば、サーバーは署名チェックをスキップしてくれるんじゃないか?」

【通常の正規トークン】
Header : {"alg": "HS256", "typ": "JWT"}
Payload: {"user": "alice", "role": "user"}
Sign   : 8a5f...(秘密鍵で作った厳格な署名)

            ↓↓↓ 攻撃者が改ざん! ↓↓↓

【攻撃用トークン】
Header : {"alg": "none", "typ": "JWT"}  <-- ここを書き換える!
Payload: {"user": "alice", "role": "admin"} <-- 管理者に昇格!
Sign   : (空っぽにする、または削除)

なぜサーバーは騙されてしまうのか?

「そんなバカな話があるわけない」と思いますよね。しかし、初期のJWTライブラリや自作の検証ロジックを組み込んだサーバーは、以下のような致命的な手順で処理を行ってしまっていたのです。

1. クライアントから届いたJWTのヘッダーを読み取る。
2. ヘッダーに alg: none と書いてあるのを見つける。
3. 「あ、今回は署名検証をしなくていいルールなんだね!」と解釈する。
4. 署名検証ステップをスルーして、ペイロードの「管理者(admin)」という情報をそのまま信じ込む。
5. 攻撃者に管理者権限を渡してしまう!

手紙の送り主が自分で「この手紙はサイン不要です」とメモを貼ったら、配達員や受取人がサインを確認せずに信用してしまったような状態です。恐ろしいですよね。

—

3. コードで見る:脆弱な実装と安全な防御策

この攻撃を防ぐ根本的な対策はとてもシンプルです。

「どんな検証方式を使うかは、クライアント(トークン)の言いなりにならず、サーバー側で固定・強制する」

これに尽きます。Pythonの人気ライブラリである PyJWT を例に、ダメなコードと正しいコードを見比べてみましょう。

脆弱なコードの例(絶対に真似しないでください!)

一昔前のライブラリや誤った実装では、受け取ったトークンに書かれているアルゴリズムを無条件に受け入れてしまうことがありました。

import jwt

# サーバーが保持している秘密鍵
SERVER_SECRET_KEY = "my-super-secret-key"

# クライアントから送られてきたトークン(攻撃者がalgをnoneに改ざんしたもの)
untrusted_token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYXR0YWNrZXIiLCJyb2xlIjoiYWRtaW4ifQ."

# 【危険な実装】
# サーバーがアルゴリズムを検証せず、トークンの言いなりになってデコードしてしまう
try:
    # algorithmsパラメータを指定しない(あるいはnoneを許可してしまう)と...
    payload = jwt.decode(
        untrusted_token, 
        key="", # noneなので鍵の検証がスキップされる
        options={"verify_signature": False} # 署名検証を無効化する最悪の設定
    )
    print(f"ログイン成功! ユーザー: {payload['user']}, 権限: {payload['role']}")
except Exception as e:
    print(f"拒否されました: {e}")

これでは、攻撃者が作った偽の管理者トークンがすんなり通ってしまいます。

安全なコードの例(ベストプラクティス)

現代の堅牢な実装では、「サーバー側が許可するアルゴリズムのホワイトリスト」を明示的に指定します。

import jwt
from jwt.exceptions import InvalidAlgorithmError, InvalidSignatureError

# サーバーが保持している秘密鍵
SERVER_SECRET_KEY = "my-super-secret-key"

# クライアントから送られてきた攻撃用トークン(alg: none)
untrusted_token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYXR0YWNrZXIiLCJyb2xlIjoiYWRtaW4ifQ."

try:
    # 【安全な実装】
    # 1. 期待するアルゴリズム(例: HS256)をリストで厳格に固定する
    # 2. 正しい秘密鍵を必ず渡す
    payload = jwt.decode(
        untrusted_token,
        key=SERVER_SECRET_KEY,
        algorithms=["HS256"] # 「HS256」以外で署名されたトークンは門前払い!
    )
    print(f"認証成功: {payload}")

except InvalidAlgorithmError:
    # algが none や未許可のアルゴリズムだった場合、ここで確実にブロックされます
    print("【防御成功】不正なアルゴリズム(noneなど)が検出されたため、リクエストを拒否しました!")

except InvalidSignatureError:
    # 署名が合致しない場合もここで弾かれます
    print("【防御成功】署名が正しくありません!")

except Exception as e:
    print(f"エラー: {e}")

このように、検証側で algorithms=["HS256"] のように固定しておけば、仮にヘッダーが alg: none に書き換えられていても、ライブラリが「許可されていない方式です!」と即座に例外(InvalidAlgorithmError)を投げて守ってくれます。

—

4. インフラ・バックエンドで守るべき実践チェックリスト

Noneアルゴリズム攻撃を完全にシャットアウトするために、日頃の開発やインフラ設計で以下の3点を意識しましょう。

  • 最新のJWTライブラリを利用する

著名なライブラリ(PyJWT、jsonwebtoken (Node.js)、jjwt (Java) など)の最新バージョンでは、デフォルトで none アルゴリズムが禁止されています。まずはライブラリが古くないか確認しましょう。

  • アルゴリズム検証を明示的に指定(ホワイトリスト化)する

コード内で検証関数を呼ぶ際は、必ず引数として algorithms=["HS256"] や ["RS256"] を明示的に指定してください。「空配列」や「指定なし」は思わぬ事故のもとです。

  • APIゲートウェイでの一次検知

バックエンドのアプリケーションに到達する前段(AWS API Gateway、Kong、Apigee、Cloudflare Workersなど)でJWTの検証を行う場合も、検証ポリシーで許可するアルゴリズムが限定されているかを必ずチェックしましょう。

—

5. まとめ

JWTの「Noneアルゴリズム攻撃」は、仕組みを知ってしまえば「そんな単純な手口だったのか!」と驚かれたかもしれません。

しかし、歴史上名だたる巨大サービスやオープンソースでも、この仕様の解釈違いによって重大な脆弱性として報告されてきた過去があります。

  • JWTは「ヘッダー」「ペイロード」「署名」の3要素で構成される
  • 攻撃者はヘッダーを alg: none に書き換えて署名検証をすり抜けようとする
  • 対策は「サーバー側で許可するアルゴリズムを明示的に固定(ホワイトリスト化)する」こと

REST APIの設計において、ステートレスで便利な仕組みを安全に運用するには、プロトコルやデータのやり取りに対する正しい理解が欠かせません。

セキュリティと聞くと難しく感じがちですが、身近な手紙やルールに例えながら、一つひとつの仕組みを丁寧に紐解いていけば怖くありません。一歩ずつ、安全で美しいWeb APIを構築していきましょう!

コメント

タイトルとURLをコピーしました