現場のエンジニア諸君、お疲れ様。
今日は、多くの開発者が「なんとなく動いているから」と放置しがちな、IDトークン(JWT)の検証における「致命的な穴」について話そう。
現場でインシデント対応をしていると、このOIDC(OpenID Connect)の実装ミスが原因で、認証がスルーされ、バックエンドが丸裸にされているケースに何度も遭遇する。特に「署名アルゴリズムの固定」と「クレーム検証の甘さ」は、攻撃者にとっての格好の入り口だ。
教科書的な話は一旦置いて、「なぜ攻撃者はそこを突くのか」「どう実装すれば防げるのか」という、泥臭い実務の話をする。
—
1. 攻撃者が狙う「署名アルゴリズムの盲点」
JWTのヘッダーには alg(アルゴリズム)というフィールドがある。多くのライブラリで「受け取ったトークンの alg を信用して検証する」という実装をしてしまうのが、最初にして最大の過ちだ。
攻撃手法:alg: none 攻撃
もしバックエンドが「alg フィールドの値を見て、そのアルゴリズムで検証する」という実装をしていたら、攻撃者はこうする。
1. トークンのヘッダーを {"alg": "none"} に書き換える。
2. ペイロードを自分の都合のいいもの(sub: admin など)に書き換える。
3. 署名部分(JWTの3番目のセグメント)を空にする。
これだけで、脆弱な検証ライブラリは「署名なしでOK」と判断して通してしまう。これは極端な例だが、同様に RS256 を期待している場所で HS256 にすり替え、公開鍵を共有鍵として扱わせることで署名を偽造する攻撃(Key Confusion Attack)も定番だ。
—
2. 署名検証とクレーム検証の実装(Python例)
「ライブラリがよしなにやってくれる」を信じるな。最低限、「アルゴリズムの強制指定」と「iss(発行者)/aud(オーディエンス)の厳格な検証」は自分たちの手で制御する。
以下は、Pythonの PyJWT を使った、現場で使うべき堅牢な検証実装だ。
import jwt
from jwt import PyJWKClient
# 1. JWKSエンドポイントから公開鍵を取得(都度ハードコーディングは厳禁)
url = "https://auth.example.com/.well-known/jwks.json"
jwks_client = PyJWKClient(url)
def verify_id_token(token):
try:
# 2. 公開鍵をJWKSから取得
signing_key = jwks_client.get_signing_key_from_jwt(token)
# 3. ここが重要:検証アルゴリズムを強制的に固定(RS256以外は受け付けない)
# 4. audとissを必ず検証する
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="my-client-id",
issuer="https://auth.example.com"
)
return payload
except jwt.ExpiredSignatureError:
print("トークンの有効期限切れ")
except jwt.InvalidTokenError as e:
print(f"検証失敗: {e}")
return None
なぜこの実装が重要なのか?
algorithms=["RS256"]: これを明示することで、攻撃者がalg: noneやHS256を指定しても、ライブラリ側で弾くようになる。audience: これがないと、他のサービス向けに発行された正当なトークンを使い回す「なりすまし」を防げない。issuer: これがないと、攻撃者が自分で立てた偽のOIDCサーバーから発行したトークンを受け入れてしまう。
—
3. なぜ「iss」と「aud」の検証をサボってはいけないのか
「うちのアプリはクローズドだから大丈夫」という甘い考えは捨ててくれ。
- aud(オーディエンス)検証の欠如: 攻撃者が、同じ認証基盤を使っている「別サービスA」で正当なユーザーとしてログインし、そこで発行されたトークンを「サービスB(君たちのアプリ)」に流し込む。検証がなければ、君たちのサービスは「あ、これ認証済みユーザーだ」と判断し、ログインを許可してしまう。これが「クロスサービス攻撃」だ。
- iss(発行者)検証の欠如: 認証基盤が複数ある環境や、将来的に連携先が増えることを想定すると、必ず発行者のURLをホワイトリストでチェックしなければならない。
—
4. 運用エンジニアへの提言:設定の守り方
コードだけでなく、インフラ側でも防御を固めるのがプロだ。
Nginxでのリクエスト制限(概念)
IDトークンが偽造されたリクエストが大量に飛んでくる場合、JWTの構造を解く前に、怪しいリクエストをWAFで落とす設定も有効だ。
# Nginx設定: 特定のパスへのリクエストでヘッダーにJWTが含まれていない場合、
# あるいはJWTの構造が明らかに異常な場合は403を返すなど
location /api/ {
if ($http_authorization !~* "^Bearer [a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_]+$") {
return 403;
}
proxy_pass http://backend_app;
}
—
まとめ:信頼は「疑うこと」から始まる
セキュリティエンジニアの仕事は、「システムが受け取るデータは、常に攻撃者によって改ざんされている可能性がある」という前提で設計することだ。
1. 署名アルゴリズムは常にホワイトリストで固定する(ライブラリのデフォルトを鵜呑みにしない)。
2. iss, aud, exp(有効期限)の検証は必須項目。これらを省略したコードは、セキュリティ上「未完成」であるとみなせ。
3. 公開鍵はJWKSから動的に取得する。鍵のローテーションに対応できない設計は、長期的には必ず破綻する。
もし君のチームで、IDトークンの検証コードが jwt.decode(token, verify=False) のような、検証をオフにするオプションを含んでいるのを見つけたら……即座に修正のプルリクエストを投げてくれ。それが、君のシステムを守る一番の近道だ。
コードは嘘をつかない。だが、設定の甘さは嘘をつき続けて、ある日突然、君たちのシステムの脆弱性として牙を剥くぞ。常に疑って、厳格に実装しろ。期待している。
コメント