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

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

値をブラウザに置く場所は 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 キーあたりの値だと誤解したりします。

localStoragesessionStorage の合計を「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.comhttp://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 のクッキー
関連リファレンス

次に読む