【実務・中級編】 認証バイパスとJWT(JSON Web Token)の脆弱性悪用 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

JWTの裏側:なぜ「ただの文字列」が世界中の認証を破壊するのか

現場でセキュリティ診断をしていると、開発者が「JWTは暗号化されているから安全」と誤解している場面に何度も遭遇する。はっきり言おう。JWTは暗号化などされていない。ただのBase64エンコードされたJSONだ。

中身は誰でもデコードして読めるし、署名を突破された瞬間に、攻撃者は「管理者」としてシステムに君臨する。今日は、JWTを巡る「穴」と、それを塞ぐための防壁を、現場のリアリティを交えて解説する。

—

1. 攻撃者が狙うJWTの3つの盲点

攻撃者は、JWTの構造を「検証する側」の甘い実装に漬け込む。特に以下の3つが頻出する脆弱性だ。

① alg: "none" 攻撃

JWTのヘッダーにある alg フィールドを none に書き換える手法だ。一部の古いライブラリや設定ミスがある実装では、alg: "none" を指定すると「署名の検証をスキップ」してトークンを有効とみなしてしまう。

  • 悪用: 攻撃者は payload を {"role": "admin"} に改ざんし、署名部分を空にしてサーバーへ送る。

② 弱いシークレットキーのブルートフォース

HS256(HMAC SHA-256)アルゴリズムを使用している場合、サーバー側が持つ「秘密鍵」が辞書攻撃に弱いと即座にゲームオーバーだ。password や secret といった推測可能なキーを使っていれば、数分でトークンは偽造される。

③ KID (Key ID) インジェクション

JWTヘッダーの kid パラメータを使って、サーバー上の任意のパスにあるファイルを読み込ませたり、悪意のあるキーを強制的に読み込ませたりする攻撃だ。OSコマンドインジェクションに近い挙動を誘発できる。

—

2. 脆弱な実装例(これだけはやるな)

まず、以下のPHPコードを見てほしい。これが脆弱性の典型だ。

// 非常に危険なコード:署名をまともに検証していない
$token = $_COOKIE['auth_token'];
$decoded = json_decode(base64_decode(explode('.', $token)[1]), true);

// 署名を検証せず、中身のroleだけを信じてアクセス権を付与している
if ($decoded['role'] === 'admin') {
    // 管理画面へアクセス許可... これが最大のミス
}

このコードの何が問題か? base64_decode して取り出したデータを「サーバーが発行したものだ」という前提で無条件に信じている点だ。JWTライブラリを使わず、自前で文字列をパースしている実装は、例外なく「脆弱性の宝庫」だ。

—

3. セキュアな実装:堅牢なJWT運用の教科書

JWTを扱う際は、「標準ライブラリ以外は絶対に使わない」「署名検証は妥協しない」というルールを徹底すること。ここではPythonの PyJWT を使った、現場で推奨される実装例を示す。

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

import jwt

# 秘密鍵は環境変数から読み込み、絶対にソースコードに直書きしない
SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
ALGORITHM = 'HS256'

def verify_token(token):
    try:
        # algorithmsでアルゴリズムを明示的に指定(noneを拒否する)
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload
    except jwt.ExpiredSignatureError:
        # トークンの有効期限切れ
        return {"error": "Token expired"}
    except jwt.InvalidTokenError:
        # 署名不一致や不正なトークン
        return {"error": "Invalid token"}

ポイント

1. algorithms=[ALGORITHM]: ここで利用可能なアルゴリズムを限定することで、none や RS256 への勝手な切り替えを防ぐ。
2. ExpiredSignatureError: トークンの有効期限(exp クレーム)を必ずチェックし、短命なトークン運用を心がける。

—

4. インフラ側で守る:WAFの防御戦略

アプリケーションレイヤーだけでなく、NginxやWAFでも多層防御を行うべきだ。

NginxによるJWTフィルタリング(設定例):
Authorization ヘッダーが異常な形式である場合、アプリケーションに渡す前にブロックする。

# 異常なJWT形式を弾く設定例
if ($http_authorization ~* "none") {
    return 403;
}

location /api/ {
    # JWTの構造を簡易的にチェック
    if ($http_authorization !~* "Bearer\s+[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+") {
        return 401;
    }
    proxy_pass http://backend_app;
}

—

最後に:エンジニアへ伝えたいこと

JWTは、認証の「鍵」そのものだ。家を建てる時に、玄関の鍵を100円ショップの南京錠にする人はいないだろう? JWTも同じだ。署名を軽視することは、玄関を全開にして泥棒を招き入れるのと同じことだ。

1. 署名を信じるな、検証の結果を信じろ。
2. 秘密鍵は長く、ランダムに生成し、ローテーションせよ。
3. ライブラリを信頼し、自作のパーサーは捨てろ。

この3つを守るだけで、攻撃者の99%は諦めて別のターゲットを探しに行くだろう。それが、我々エンジニアが守るべきプロの境界線だ。

コメント

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