localStorageにJWTを保存するのは悪なのか

Tech

Xでの色んな意見まとめ

Xで見かけたこのポスト

https://x.com/cordx56/status/2057861124209742275?s=20

リプや引用が勉強になったのでメモ代わりにまとめます。

JWT保存場所(localStorage vs httpOnly Cookie)まとめ

前提:そもそも何が問題か

  • localStorageにJWTを保存するとXSSでトークンそのものを盗み出せることが最大のリスク
  • httpOnly CookieにしてもXSSで任意のAPIリクエストは投げられるが、トークンの持ち出しは防げる

→ この差が重要!

議論の前提として:

  • localStorageとCookieのどちらが安全か、という比較を「無条件により安全な方を選ぶ」文脈で語ることには違和感がある、という意見も
  • JWTを複数ドメインに送りたい・認証ヘッダーが必須・既存サーバーに手を出せないなど、アーキテクチャ上の理由があってlocalStorage+JWTを選ぶなら、それは合理的な判断になりうる

→ システムをシンプルに構築することが第一で、セキュリティ懸念の解決はその後という順序で考えるのが本来の設計の進め方。

攻撃ベクター別の比較

✅ = 防御可能 / ❌ = 脆弱

脅威 localStorage httpOnly Cookie
XSSによるトークン盗取 ❌ 可能(JSで直接読める) ✅ 防げる(JSからアクセス不可)
XSSによる不正APIリクエスト ❌ 可能 ❌ 可能(Cookieはブラウザが自動送信)
CSRF ✅ 自動送信されない ❌ 要対策(SameSite / CSRFトークン)
トークン持ち出し後の長期悪用 ❌ 期限切れまで別端末から使い続けられる ✅ 被害は「被害者ブラウザ上・その場限り」

なぜトークン盗取と不正リクエストで差があるか

  • 盗取されたトークン:攻撃者が自分の環境から、被害者がページを閉じた後も、期限切れまで使い続けられる
  • XSSによるin-place攻撃:被害者のブラウザ上でしか動けず、スクリプトが止まれば(タブを閉じるなど)攻撃能力も消える

→ httpOnly Cookieは被害の深さを一段浅くする防御レイヤー。

ベストプラクティス(非SSR SPAでの現実解)

トークン種類別:

  • Access Token:メモリ保持(またはhttpOnly短命Cookie)
  • Refresh Token:httpOnly + SameSite=Strict[1] + Path制限 + サーバー側で失効管理

攻撃別:

  • XSS対策:CSPでインラインスクリプト禁止 + 入力サニタイズ(これが最重要)
  • CSRF対策:SameSite=Lax/Strict[1:1] + CSRFトークン or Double Submit Cookie[2]

→ 「localStorageが悪でCookieが善」ではなく、脅威モデルに合わせた組み合わせが重要。

実務上の補足

  • httpOnly CookieはSSRがなくても使える(fetchcredentials: 'include'するだけ)
  • SafariはITP(Intelligent Tracking Prevention)[3]により、条件によっては7日間アクセスがないとlocalStorageがクリアされる場合がある(UX上のリスク)
  • WAFの誤検知確認も実務では必要

結論

  • XSSを防ぐことが最重要
  • トークンの保存場所はあくまで被害の深さを変える保険
  • localStorageはトークン盗取リスクが高く非推奨
  • httpOnly CookieでもXSSによる不正リクエストは防げないが、被害範囲の限定という点で防御レイヤーとして意味がある

感想

なぜかこの議論だと過激派(?)が現れる謎。

脚注
  1. Cookie属性の一つ。Strictは完全に別サイトからのリクエストにCookieを送らない、Laxはトップレベルナビゲーション(リンククリック等)のGETのみ許可する設定。 ↩︎ ↩︎

  2. CSRFトークンをCookieとリクエストボディ(またはヘッダー)の両方に同じ値で送り、サーバー側で一致確認する方式。サーバー側でトークンを保存する必要がないのが利点。 ↩︎

  3. Safari/WebKitのプライバシー保護機能。クロスサイトトラッキング抑止のため、特定条件下でCookieやlocalStorage等のスクリプト書き込み可能ストレージに有効期限の上限を課す。 ↩︎