【実務・中級編】 API認可におけるJWT(JSON Web Token)の脆弱性とベストプラクティス – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

JWTは「魔法の杖」ではない:現場で戦うエンジニアのための防弾実装ガイド

「JWTを使えば認証は安泰だ」と信じている若手エンジニアを、私は何人も見てきた。しかし、インシデント対応の最前線に立つ人間から言わせれば、JWTは適切に扱わなければ「自ら鍵を投げ渡すようなもの」だ。

今回は、JWTの実装で最も頻発し、かつ致命的な脆弱性である alg: none 攻撃を中心に、現場で即座に適用すべき防御策を、泥臭いコードと共に叩き込む。

—

1. なぜJWTは狙われるのか:alg: none の恐怖

JWTの最大の罠は、ヘッダーに記述された alg(アルゴリズム)フィールドを検証側が安易に信頼してしまうことにある。

攻撃者は、JWTのペイロードを改ざんし、ヘッダーを以下のように書き換えて送りつけてくる。

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

ライブラリの設定が甘いと、サーバー側は「お、algがnone(署名なし)か。じゃあ検証は不要だな」と判断し、改ざんされたペイロードを正当なものとして受理してしまう。これが alg: none 攻撃だ。これを許すということは、家の鍵の形状を「鍵なしで開く」という仕様に変更して泥棒を招き入れるのと同じことだ。

—

2. 【防衛策】アルゴリズムを「固定」せよ

一番の教訓は、「受信したトークンのヘッダーにある alg を信用するな」ということだ。サーバー側で許可するアルゴリズムをあらかじめハードコーディングしておき、それ以外は即座に拒絶する。

Python (PyJWT) によるセキュアな検証例

多くのエンジニアが犯すミスは、アルゴリズム指定を省略することだ。以下のように、明示的に期待するアルゴリズムを指定しなければならない。

import jwt

# 攻撃を防ぐため、必ず許可するアルゴリズムをリストで明示する
ALLOWED_ALGS = ["RS256"] 

def verify_token(token, public_key):
    try:
        # algorithms引数を省略してはいけない。none攻撃を無効化する
        payload = jwt.decode(
            token, 
            public_key, 
            algorithms=ALLOWED_ALGS
        )
        return payload
    except jwt.InvalidAlgorithmError:
        # 許可されていないアルゴリズムが指定されたら即座にログを吐いて破棄
        print("警告: 不正なアルゴリズムが検出されました")
        return None
    except jwt.ExpiredSignatureError:
        print("トークンの有効期限切れ")
        return None

—

3. 秘密鍵のローテーション:もしも「漏洩」したら?

RSAやECDSAを用いた公開鍵暗号方式を採用していても、秘密鍵が流出したら終わりだ。しかし、多くのシステムでは「秘密鍵のローテーション」が運用計画から抜け落ちている。

実務におけるベストプラクティス:
1. 鍵ID (kid) の活用: JWTのヘッダーに kid を含め、どの鍵で署名したかを識別できるようにする。
2. jwks_uri の公開: 鍵を更新する際は、新しい公開鍵をJWKS(JSON Web Key Set)エンドポイントで提供し、検証側が自動的に鍵を更新できるようにする。

Nginxでの検証(API Gateway層でのガード)

アプリケーション層に到達する前に、Nginxで署名の形式をチェックするのも有効な多層防御だ。複雑なロジックはLua等で行うが、基本的なヘッダーチェックは以下の設定を参考にせよ。

# Nginxの設定例:不正なペイロードサイズを弾く
location /api/ {
    # JWTは長大になりがちだが、無制限に受け入れるとDoSのリスクがある
    client_max_body_size 16k;
    
    # ここでJWTヘッダーのバリデーションをゲートウェイで行うのが理想
    # Luaモジュールを利用して、alg: noneが含まれていないかチェックする
    access_by_lua_block {
        local jwt = require("resty.jwt")
        -- 署名検証ロジックをここに記述
    }
}

—

4. 現場の教訓:これだけは守れ

私が数々の事故現場を見てきて断言できるのは、「ライブラリのデフォルト設定を疑え」ということだ。

  • アルゴリズムは固定: alg の動的切り替えは絶対に許さない。常に RS256 や ES256 などの非対称暗号を使い、HS256(共通鍵)の使い回しは避ける。
  • 鍵の管理: 秘密鍵をソースコードに埋め込むのは論外だ。AWS Secrets ManagerやHashiCorp Vaultなど、専用の鍵管理サービスを利用し、ローテーションを自動化せよ。
  • トークンの短命化: JWTは一度発行すると取り消し(無効化)が難しい。アクセス権限を示すトークンは5分〜15分程度の短命にし、リフレッシュトークンで運用する設計が基本だ。

セキュリティは「完成」しない。だが、上記のような「詰めの甘さ」を一つずつ潰していくことで、攻撃者にとって「割に合わないターゲット」になることはできる。

君たちの書くコードが、次のインシデントを防ぐ最後の砦になる。迷ったら、常に「最悪のシナリオ」を想定して実装することだ。それが、エンジニアとしての矜持というものだ。

コメント

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