JWTの脆弱性は「署名」の信頼をどこまで疑えるかが勝負だ
現場でコードレビューをしていると、JWT(JSON Web Token)を単なる「暗号化された箱」だと思い込んでいるエンジニアに遭遇することがある。断言するが、JWTに暗号化機能はない。 あれはただの「署名付きのBase64エンコードされたJSON」だ。
署名検証が正しく実装されていないJWTは、金庫の鍵をかけたふりをして、実は付箋に暗証番号を書いて扉に貼っているようなものだ。今日は、攻撃者がどうやってその「扉」をこじ開けるのか、そしてなぜ我々がそれを防がなければならないのかを解説しよう。
1. 攻撃者の視点:なぜ alg: none は今でも刺さるのか
JWTのヘッダーにある alg パラメーターは、サーバーに対して「どのアルゴリズムで署名を検証すべきか」を指示する。攻撃者が考えるのはシンプルだ。
「もし、このパラメーターを none に書き換えたら、サーバーはどう反応するだろう?」
多くの脆弱なライブラリや実装では、alg: none を受け取ると「署名の検証は不要」と判断し、ペイロード(ユーザーIDや権限情報)をそのまま信用してしまう。これがいわゆる alg: none 攻撃だ。
攻撃の手順(PoC)
1. 本物のJWTを用意する(例: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.signature)。
2. ヘッダーを {"alg":"none","typ":"JWT"} に書き換える。
3. ペイロードを {"sub":"admin"} に書き換える。
4. Base64URLエンコードし、末尾の署名部分を削除(または空のドットのみにする)して送信する。
サーバーがこの署名なしのトークンを検証して admin としてログインさせてしまったら、ゲームオーバーだ。
2. セキュアな実装の鉄則
「ライブラリがよしなにやってくれる」という考えを今すぐ捨てろ。JWTを扱う際、開発者が守るべきルールは以下の3点だ。
1. アルゴリズムの固定(ホワイトリスト): サーバー側で受け入れるアルゴリズムを RS256 等にハードコーディングし、トークン内の alg を信用しない。
2. 秘密鍵の厳重管理: HS256(対称鍵)は鍵が漏れたら終わりだ。可能な限り RS256(非対称鍵)を使用せよ。
3. 検証ライブラリの設定: 検証時に必ず期待するアルゴリズムを指定する。
実装例:Python (PyJWT) での防御
多くのバグは「ライブラリのデフォルト設定」をそのまま使うことで発生する。明示的にアルゴリズムを制限することが必須だ。
import jwt
# サーバー側で許可するアルゴリズムを固定する
ALLOWED_ALGORITHM = "RS256"
def verify_token(token, public_key):
try:
# algorithms引数を指定しないと、脆弱なライブラリはトークン内のalgを信じてしまう可能性がある
payload = jwt.decode(
token,
public_key,
algorithms=[ALLOWED_ALGORITHM]
)
return payload
except jwt.InvalidAlgorithmError:
# 許可していないアルゴリズムが来た場合は即座に拒否
raise Exception("不正なアルゴリズムです")
except jwt.ExpiredSignatureError:
raise Exception("トークンの有効期限切れ")
except Exception as e:
raise Exception(f"認証失敗: {str(e)}")
実装例:PHP (firebase/php-jwt) での防御
PHPの場合も同様、decode メソッドにアルゴリズムの配列を渡すことが生存戦略となる。
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// 公開鍵を使って検証する(RS256)
$publicKey = file_get_contents('public.pem');
try {
// 第3引数でアルゴリズムを明示的に指定する
$decoded = JWT::decode($token, new Key($publicKey, 'RS256'));
} catch (Exception $e) {
// ログに詳細を残し、ユーザーには汎用的なエラーを返す
error_log($e->getMessage());
die("認証エラー");
}
3. インフラ層での防御:WAFという最後の砦
コードレベルでの修正がすぐに行えない場合、WAF(Web Application Firewall)で「alg": "none"」という文字列が含まれるリクエストを遮断するルールを適用すべきだ。これは緊急時の泥臭い対応だが、非常に効果的である。
Nginx/ModSecurityの例(簡易ルール):
# リクエストボディに alg:none が含まれていたらブロックする
SecRule REQUEST_BODY "alg\"\\s*:\\s*\"none\"" \
"id:10001,phase:2,deny,status:403,msg:'JWT alg:none attack detected'"
最後に:セキュリティは「疑うこと」から始まる
JWTの脆弱性は、攻撃者が「システムの仕様を逆手に取れる」という楽しさを感じてしまう典型的なケースだ。あなたが書くコードが、攻撃者に「あ、このサーバーはちゃんと検証しているから無理だ」と思わせる防波堤になる。
- 署名アルゴリズムは必ず固定する。
- ユーザーからの入力を盲信しない。
- 鍵の管理は環境変数や専用のKMS(Key Management Service)で行う。
これらは基本だが、現場では驚くほど疎かになっている。今すぐプロジェクトのソースコードを検索し、jwt.decode や JWT::decode の箇所に algorithms 引数が正しく設定されているか確認してほしい。それが今日のあなたのミッションだ。
コメント