【実務・中級編】 JWT (JSON Web Token) の安全な署名検証とアルゴリズムの固定化対策 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

おい、最近のコードレビューでまた見つけたぞ。認証トークンに JWT (JSON Web Token) を使っているシステムで、署名の検証をサボっていたり、アルゴリズムの固定化(Algorithm Confusion)対策が抜けている実装が多すぎる。

「ライブラリがよしなにやってくれるだろう」なんて甘い考えで設計していると、攻撃者に裏口をこじ開けられ、全ユーザーの権限を奪われるのは時間の問題だ。特にステートレスな認証の要であるJWTは、一度実装を誤ると致命的なセキュリティインシデントに直結する。

今回は、数々のインシデント現場を踏んできた俺が、JWTの安全な署名検証と、世間を騒がせた none アルゴリズムの罠、そして公開鍵管理のリアルな鉄則を叩き込んでやる。手を動かすエンジニアなら、明日から自分のコードをどう直すべきか、最後まで読めば痛いほどわかるはずだ。

—

1. JWTが狙われる理由:なぜ攻撃者は署名を迂回できるのか?

JWTは、ヘッダー(Header)、ペイロード(Payload)、署名(Signature)の3つのパートがドット(.)で連結された非常にシンプルな構造をしている。

header.payload.signature

開発者が最も勘違いしやすいのが、「ペイロードはBase64Urlエンコードされているだけで、暗号化されているわけではない」という点だ。中身は誰でもデコードして丸見えである。そのため、機密情報をペイロードに含めること自体がナンセンスなのだが、それ以上に深刻なのが「署名の検証プロセスにおける実装ミス」だ。

攻撃者が狙う主な脆弱性パターンは以下の2つだ。

1. none アルゴリズムの許容:
ヘッダーの alg(Algorithm)パラメータに none を指定することで、署名検証を無効化し、任意のペイロード(例:isAdmin: true に改ざんしたもの)をシステムに送り込む手法。
2. アルゴリズムの混同(Algorithm Confusion / Key Confusion):
非対称暗号(RSAやECC)の公開鍵を、対称鍵(HMAC / HS256)の「シークレットキー」として誤認させる攻撃。攻撃者は公開鍵を使って自前で有効なHMAC署名を作り、検証側を突破してしまう。

—

2. 脆弱な実装の何が危ないのか?(実例とリスク)

まずは、よくある「やってはいけない実装」を見てみよう。以下のNode.js(jsonwebtoken)のコードは、セキュリティレビューで一発レッドカードを食らう典型例だ。

【危険な実装例】

const jwt = require('jsonwebtoken');

// ❌ 危険:アルゴリズムの検証を行わず、渡されたトークンをそのまま信頼している
function dangerousVerify(token, secretOrPublicKey) {
    try {
        // algorithmsオプションを指定していないため、
        // 攻撃者がヘッダーを改ざんして攻撃を仕掛けられる余地がある
        const decoded = jwt.verify(token, secretOrPublicKey);
        return decoded;
    } catch (err) {
        return null;
    }
}

このコードの何がまずいかというと、algorithms のホワイトリスト指定を怠っている点だ。これだと、攻撃者がRSAの公開鍵を入手できる環境(例えばHTTPSの証明書やJWKSエンドポイント)において、アルゴリズムを HS256 に書き換え、RSAの公開鍵を「HMACのシークレット」として使って署名を偽造できてしまう。

—

3. 完全防御のためのセキュアな実装サンプル(Node.js / Python)

では、実務でどう書くべきか。ここでは、アルゴリズムの厳格な固定化、none の完全拒否、そして適切な公開鍵管理を盛り込んだセキュアな実装を提示する。

Node.js(jsonwebtoken)によるセキュアな検証実装

非対称暗号(RS256)を使用する場合の、模範的なコードがこれだ。

const jwt = require('jsonwebtoken');
const fs = require('fs');

// 信頼できる公開鍵をファイルシステム等から安全に読み込む
const publicKey = fs.readFileSync('/path/to/public.pem', 'utf8');

function secureVerifyJWT(token) {
    try {
        const verifiedPayload = jwt.verify(token, publicKey, {
            // 【最重要】使用を許可するアルゴリズムを明示的にホワイトリスト化する
            // これにより、noneアルゴリズムや意図しない暗号方式を完全にシャットアウトする
            algorithms: ['RS256'],
            
            // 発行者(Issuer)の検証
            issuer: 'https://auth.example.com',
            
            // 聴衆(Audience)の検証
            audience: 'https://api.example.com',
            
            // 有効期限(exp)の厳密なチェック(デフォルトで有効だが明示すると吉)
            maxAge: '1h' 
        });

        return {
            success: true,
            data: verifiedPayload
        };
    } catch (err) {
        // ログには詳細を記録しつつ、クライアントには汎用的なエラーを返す
        console.error(`JWT Verification Failed: ${err.message}`);
        return {
            success: false,
            error: 'Invalid or expired token.'
        };
    }
}

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

Pythonの PyJWT を使う場合も同様だ。デフォルトの挙動に頼らず、許容するアルゴリズムを明示的に指定する必要がある。

import jwt
from jwt.exceptions import PyJWTError

# 信頼できるRSA公開鍵
PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
"""

def secure_verify_jwt(token: str):
    try:
        # 署名の検証とクレームのチェックを同時に行う
        payload = jwt.decode(
            token,
            PUBLIC_KEY,
            # 【最重要】RS256のみを許可(HS256やnoneを一切許さない)
            algorithms=["RS256"],
            # クレームの検証設定
            options={
                "verify_signature": True,
                "verify_exp": True,
                "verify_iss": True,
                "verify_aud": True,
            },
            issuer="https://auth.example.com",
            audience="https://api.example.com",
        )
        return {"success": True, "data": payload}
        
    except PyJWTError as e:
        # 偽造トークンや期限切れ、改ざんはすべてここでキャッチする
        print(f"Security Alert - JWT Error: {str(e)}")
        return {"success": False, "error": "Authentication failed."}

—

4. 現場で絶対に守るべき運用・設計の鉄則

コードレベルの対策だけでは、インフラや鍵のライフサイクル管理に穴があれば意味がない。最後に、シニアチーフとしてチームに必ず徹底させている運用ルールを共有しよう。

1. none アルゴリズムの処理をライブラリレベルで禁止する
自前でJWTのパーサを書くような愚行は絶対に避け、セキュリティパッチが継続的に適用されている信頼性の高いオープンソースライブラリ(jsonwebtoken, PyJWT, go-jose 等)を使用すること。その上で、常に algorithms パラメータを明示する習慣をつけろ。
2. 公開鍵(JWKS)のキャッシュとローテーション
RSAやECCの公開鍵をハードコーディングするのは論外だ。Auth0やAWS Cognito、自前のIdPが提供する JWKS (JSON Web Key Set) エンドポイントを利用し、定期的に公開鍵を自動取得・キャッシュする仕組みを構築せよ。鍵のローテーション(漏洩時の迅速な無効化)ができないシステムは、セキュリティ設計の欠陥と言わざるを得ない。
3. ペイロードには機密情報を載せない原則の徹底
前述の通り、JWTは暗号化されていない。パスワードハッシュ、個人情報(PII)、クレジットカード情報などをペイロードに含めることは、データベースのダンプよりも容易に情報をばら撒く行為に等しい。識別子(sub や user_id)と権限の最低限のスコープだけを載せるのが鉄則だ。

—

まとめ

JWTは正しく使えば非常に強力でスケーラブルな認証基盤になるが、一歩間違えれば「攻撃者にフリーパスを渡す道具」に成り下がる。

「動けばいい」という実装から脱却し、「アルゴリズムのホワイトリスト化」「noneアルゴリズムの排除」「厳格なクレーム(iss, aud, exp)の検証」の3点セットを、今日から君のプロジェクトの標準仕様として組み込んでほしい。

セキュリティは細部に宿る。妥協のないコードで、セキュアなシステムを作り上げていこう。

コメント

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