メインコンテンツまでスキップ

ブラウザの保存領域: どこに置くかで何が変わるか

値をブラウザに置く場所は 1 つではありません。どれを選ぶかで、サーバーに届くか、別タブから見えるか、いつ消えるかが変わります。

置き場を間違えたときの症状は分かりにくい形で出ます。ログイン状態が別タブに伝わらない、http では動くのに https で値が消える、容量が足りないと言われる。どれも「保存できたかどうか」ではなくどの単位で分かれているかの問題です。

この章で学ぶこと​

  • 3 つの機構を「サーバーへ届くか」「何で分かれるか」「いつまで残るか」「容量の制限がどうかかるか」「タブ間で共有されるか」の 5 軸で比べる
  • クッキーと Web Storage で分かれる単位が正反対であること
  • 容量の数値が下限と上限で向きが逆であること
  • 認証トークンをどこに置くか
前提知識

オリジンと CORS のオリジンの 3 つ組と、クッキー の属性を前提にします。この章はその 2 つの境界に、保存領域という 3 つ目を並べます。

この章で扱わないこと​

観点参照先
5 MiB に収まらないデータと、オフラインで動かす仕組みIndexedDB と Cache API
保存したデータが消える条件 (退避の機構)同上
他のサイトに埋め込まれたときの分かれ方同上。実装はオリジンだけを鍵にしていない
SPA でのトークン保管とリフレッシュフローの実装React ガイド — SPA の認証
クッキーの属性そのものの意味クッキー

3 つの機構を 5 つの軸で比べる​

比べる対象は localStorage / sessionStorage / クッキーの 3 つです。軸ごとに小さい表を置きます。1 つの大きな表にまとめると、軸によって意味の違う値が同じ列に並んで誤読を生みます。

1. サーバーへどう届くか​

機構サーバーへの届き方
localStorage届かない。コードが明示的にボディやヘッダーへ載せたときだけ
sessionStorage同上
クッキーブラウザが条件に合えば自動で付ける

差がはっきり出るのはサーバーサイドレンダリングの初回 HTML です。サーバーが最初の HTML を組み立てる時点で読めるのはクッキーだけで、Web Storage はブラウザの中にしかありません。初回描画をログイン状態で出し分けたいなら、判定材料はクッキーに置く必要があります。

2. 何で分かれるか​

機構分ける鍵schemeport
localStorage / sessionStorageオリジン含む含む
クッキーhost + path見ない (Secure で絞れる)見ない

26 章は「クッキーのスコープはオリジンではない」と書きました。Web Storage はその逆で、オリジンそのものです。同じ host でも scheme が違えば別の領域になります。

https://example.com で書いた localStorage の値は
http://example.com からは読めない (別のオリジン)

MDN は「for a site loaded over HTTP … localStorage returns a different object than … over HTTPS」と述べています。port については仕様が storage key をオリジンの組と定めるので含まれますが、MDN が挙動として明示するのは scheme だけです。

この非対称がこの部の主題です。 同じ「保存する」でも、クッキーは scheme と port を無視して広く届き、Web Storage は両方を見て狭く分かれます。片方の感覚でもう片方を扱うと事故になります。

3. いつまで残るか​

機構消えるとき
localStorage期限を持たない
sessionStorageタブを閉じるまで
クッキーExpires / Max-Age の指定時。どちらも無ければブラウザを閉じるまで

MDN は localStorage について「the stored data is saved across browser sessions」「localStorage data has no expiration time」と述べています。

ただし期限を持たないことは、消えないことではありません。ブラウザは保存領域が逼迫すると古いものから捨てますし、追跡防止の一環で一定期間使われていないデータを消す実装もあります。この機構は IndexedDB と Cache API が扱います。

4. 容量の制限がどうかかるか​

数値だけを並べると誤読します。何に対する値かと、下限か上限かを各行に書きます。

機構値何に対する値か向き
localStorage5 MiBオリジンごとの領域全体上限
sessionStorage5 MiB同上上限
クッキー4096 バイト1 個あたり (名前 + 値 + 属性の合計)下限
クッキー50 個1 ドメインあたり下限

クッキーの値は RFC 6265 が定める「実装が満たすべき最小要件」で、上限ではありません。実装はこれ以上を許してかまいません。一方 Web Storage の 5 MiB は Storage Standard が保存先ごとに割り当てる上限です。

かかる対象が「1 個」と「領域全体」で違い、向きも逆です。 同じ制限として読むと、クッキーに 4 KB 弱まで詰め込めば安全だと考えたり、Web Storage の 5 MiB を 1 キーあたりの値だと誤解したりします。

localStorage と sessionStorage の合計を「10 MiB」と数えることもできません。sessionStorage はタブごとに分かれるので、オリジンあたりの合計として足し合わせる意味がないからです。

5. タブ間で共有されるか​

機構別のタブから見えるか
localStorage見える。他タブの変更は storage イベントで届く
sessionStorage見えない。タブごとに独立する
クッキー見える

sessionStorage を分ける単位は、仕様では「タブ」ではなく traversable navigable です。ブラウザの UI としてのタブとほぼ一致しますが、規定しているのは閲覧の履歴をたどれる単位のほうです。Storage Standard は「Session storage buckets must be cleared as traversable navigables are closed」と定めます。

MDN は「Opening a page in a new tab or window creates a new session … which differs from how session cookies work」と述べています。window.open で開いた先には開いた時点の値がコピーされますが、以後は別々に動きます。

HttpOnly は保存先ではありません​

HttpOnly を「4 つ目の保存場所」のように並べる説明を見かけますが、これはクッキーの属性です。上の表でクッキーの行に含まれます。

そして塞ぐのは読み取りだけです。クッキーはリクエストに自動で付くので、XSS が起きたスクリプトは値を読めなくても、そのクッキーが付いたリクエストを送れます。26 章の「よくある誤解」が打ち消したこの誤読を、保存先の比較表で復活させないでください。

保存しないという選択​

値を JavaScript の変数に持つだけで、どこにも保存しない方法があります。上の 5 軸の表には並べていません。軸 1 では Web Storage と同じ、軸 3 に該当する値がなく、軸 4 と軸 5 も空欄になるからです。

ただし認証トークンの置き場を考えるときは比較対象になります。

性質メモリ保持
ページを再読み込みしたとき消える
ディスクに残るか残らない
XSS で読まれるか読まれる (同じ実行コンテキストにある)

「保存しない」は XSS への対策にはなりません。効くのは、端末に残った値を後から拾われる経路だけです。

認証トークンをどこに置くか​

OWASP の Session Management Cheat Sheet は、資格情報の保存先を名指しで禁じています。

Do not store authentication tokens, session IDs, JWTs, refresh tokens, or any credential in localStorage or sessionStorage.

理由は軸 5 と HttpOnly の話につながります。Web Storage は JavaScript から読めるので、XSS が起きた時点で値が持ち出されます。HttpOnly のクッキーなら値は読めません。持ち出されないぶんだけ被害が小さくなります。

置き場を決める手順は次のとおりです。

短命なアクセストークンをメモリに持ち、長命なリフレッシュトークンを HttpOnly のクッキーに置く構成が、この手順の帰結です。React ガイド — SPA の認証 が実装を扱います。

5 MiB に収まらないときは​

3 つの機構はどれも大きいデータに向きません。理由は機構ごとに違います。

  • Web Storage は領域全体で 5 MiB が上限で、API も同期です。大きい値を読み書きするとその間メインスレッドが止まります
  • クッキーの制限はさらに小さく、しかもリクエストに自動で付くので、大きい値を置くと通信そのものが重くなります

「5 MiB に収まらないもの」と「オフラインで動かしたいもの」が次の章の入口です。

よくある誤解​

「localStorage は https と http で同じ値を共有する」 — しません。Web Storage の鍵はオリジンで、scheme を含みます。クッキーの感覚で考えると取り違えます。

「クッキーは 1 ドメイン 4096 バイトまで」 — 4096 バイトは 1 個あたりで、しかも実装が満たすべき下限です。個数の下限は 1 ドメイン 50 個なので、単純に掛ければ 200 KB 近くになります。

「sessionStorage はブラウザを閉じるまで残る」 — タブを閉じるまでです。同じオリジンを別タブで開いても値は共有されません。

「localStorage は期限がないので消えない」 — 消えます。期限の指定がないだけで、容量の逼迫や追跡防止の削除の対象です。

「トークンを sessionStorage に置けば localStorage より安全」 — XSS に対しては同じです。どちらも JavaScript から読めます。違うのは残る期間だけで、攻撃が成立する瞬間の防御力は変わりません。

確認問題​

問 1. https://example.com で次のコードを実行しました。その後 http://example.com を開くと、localStorage.getItem('theme') は何を返しますか。

localStorage.setItem('theme', 'dark');
解答と解説

答え: null を返します。

Web Storage を分ける鍵はオリジンで、scheme を含みます。https://example.com と http://example.com は別のオリジンなので、別々の領域を持ちます。

同じ host なので、クッキーであれば Secure を付けない限り両方へ送られます。同じ「ブラウザに置く」でもクッキーと Web Storage で結果が逆になるのがこの章の主題です。

ローカル開発で http://localhost を使い、本番で https を使っている場合、この差が「本番だけ設定が引き継がれない」形で出ます。

問 2. ユーザーの下書きを保存する機能を作ります。1 件あたり 200 KB 程度で、最大 30 件です。localStorage を選んでよいですか。

200 KB × 30 件 = 約 6 MB
解答と解説

答え: 選べません。オリジンごとの上限 5 MiB を超えます。

localStorage の 5 MiB は領域全体にかかる上限です。1 キーあたりではないので、30 件の合計で判定します。約 6 MB は上限を超えるため、書き込みの途中で例外が投げられます。

さらに localStorage の API は同期なので、200 KB の文字列を読み書きするあいだメインスレッドが止まります。件数が上限内に収まっていても、この点だけで避ける理由になります。

この規模のデータは IndexedDB が引き受けます。非同期で、容量の制限も別の体系です。次の章で扱います。

問 3. ログイン後にアクセストークンを localStorage に保存する実装をレビューしています。「XSS 対策として CSP を入れているので問題ない」と説明されました。どう答えますか。

解答と解説

答え: CSP は XSS を減らしますが、ゼロにはしません。成立したときの被害の大きさが置き場で変わります。

OWASP は「Do not store authentication tokens, session IDs, JWTs, refresh tokens, or any credential in localStorage or sessionStorage.」と述べています。この指針は CSP の有無を前提にしていません。

比較するのは「XSS が起きる確率」ではなく「起きたときに何が持ち出されるか」です。

置き場XSS が成立したとき
localStorage値そのものを読み出して外部へ送れる
HttpOnly のクッキー値は読めない。ただしそのクッキーが付いたリクエストは送れる

どちらも被害は出ますが、値を持ち出されると攻撃者の手元でトークンを再利用できます。ブラウザを閉じた後も、別の端末からも使えます。HttpOnly なら攻撃はそのページのセッション中に閉じます。

短命なアクセストークンをメモリに持ち、リフレッシュトークンを HttpOnly のクッキーに置く構成が、この差を踏まえた設計です。

まとめ​

  • 3 つの機構は「サーバーへ届くか」「何で分かれるか」「いつまで残るか」「容量の制限がどうかかるか」「タブ間で共有されるか」で選び分ける
  • クッキーと Web Storage は分かれる単位が正反対。クッキーは scheme と port を見ず、Web Storage はオリジンなので両方を見る
  • 容量の数値は向きが逆。クッキーの 4096 バイトは 1 個あたりの下限、Web Storage の 5 MiB は領域全体の上限
  • sessionStorage はタブごとに独立する。別タブへは伝わらない
  • 期限を持たないことは消えないことではない
  • HttpOnly は保存先ではなくクッキーの属性で、塞ぐのは読み取りだけ
  • 資格情報は Web Storage に置かない。短命なトークンはメモリ、長命なものは HttpOnly のクッキー
関連リファレンス

次に読む​