ブラウザのストレージ — 読めるか、と、送られるかは別の話
Cookie / localStorage / sessionStorage / IndexedDB を容量ではなく「誰が読めて、いつ送られるか」で分ける。認証情報の置き場所を決めるための判断軸。
この記事の進み方
What — 4つの置き場は、何が違うのか
ブラウザにデータを置く場所は4つあります。容量の違いで語られがちですが、事故に直結するのはそこではありません。まず並べて見てください。
サーバーへ送るために生まれた置き場
Set-Cookie: sid=abc; HttpOnly; Secure; SameSite=Lax- 容量
- 約4KB / 件
- 寿命
- 属性で指定(セッション〜年単位)
- JS から読める
- HttpOnly なら読めない
- 自動で送られる
- 同じサイトへの毎リクエスト
- 単位
- ドメイン + パス(オリジンではない)
- –毎リクエストに乗るので、大きいものを入れると全通信が重くなる
- –勝手に送られるからこそ CSRF が成立する。SameSite はその蓋
- –サブドメイン間で共有できる。逆にポートやスキームでは分かれない
容量と寿命は調べればすぐ出てくる。実際に判断を分けるのは「JS から読めるか」と「自動で送られるか」の2行。
- オリジン
- スキーム + ホスト + ポートの3点セット。Web Storage と IndexedDB はこの単位で分かれる。
- HttpOnly
- Cookie の属性。付けると JavaScript から読めなくなる。サーバーとブラウザの間だけで完結する。
- SameSite
- Cookie の属性。他サイトからの遷移やリクエストに、この Cookie を乗せるかどうかを決める。
- XSS
- そのページの権限で任意の JavaScript を実行される状態。自分で書いたコードと、混入したコードを、ブラウザは区別しない。
事故の形が違う
4つのうち3つは「JS から常に読める」でした。ここが冒頭の事故の入口です。
混入したスクリプトは、正規のコードと同じ権限で動く。localStorage は誰が呼んだかを区別しない。
同じ XSS が起きても、HttpOnly の Cookie に入れていれば図の右半分が消えます。スクリプトは Cookie を読めないので、外へ持ち出せません。
ただし攻撃が終わるわけではありません。読めなくても、そのブラウザから API を叩けば Cookie は自動で付きます。攻撃者は利用者のタブの中で操作を代行することになります。
| localStorage に置く | HttpOnly Cookie に置く | |
|---|---|---|
| XSS で読めるか | 読める | 読めない |
| 持ち出せるか | 持ち出せる | 持ち出せない |
| その場で悪用できるか | できる | できる |
| 被害が続く時間 | トークンの寿命いっぱい | タブを閉じるまで |
| 攻撃の痕跡 | 見覚えのない IP | 利用者本人の IP |
どちらも負けます。負け方が違うだけです。 この差を「安全 / 危険」の二択に潰してしまうと、次の判断を間違えます。
ログイン中だけ有効で、タブを閉じたら消えてほしい値があります。JS から読み書きし、サーバーには送りたくありません。どこに置きますか?
Why — なぜ4つもあるのか
最初にあったのは Cookie だけです。1994年、状態を持たない HTTP に記憶を持たせるために作られました(security/cookie-session で扱った話です)。
目的が「サーバーに覚えていてもらう」だったので、Cookie は送られることを前提に設計されました。これが今も効いている3つの性質を決めています。
毎リクエストに乗る。 だから大きくできません。約4KBという制限は、通信量の問題として当然の帰結です。
勝手に乗る。 利用者がリンクを踏んだだけでも、フォームが送信されただけでも乗ります。この「勝手に」が CSRF の成立条件で、SameSite はその蓋として後から足されました。
サーバーが属性で制御できる。 HttpOnly を付ければ JS から隔離できます。サーバーが配ってサーバーが受け取る、という一本道に閉じられるのが Cookie の強みです。
送りたくないものが増えた
2000年代後半、ブラウザの中でアプリが動くようになると、サーバーに送る必要のないデータが増えました。表示設定、下書き、直前の検索条件。これらを Cookie に入れると、関係のない全リクエストに乗って帯域を食います。
そこで Web Storage が入りました。設計は Cookie の裏返しです。送らない。属性もない。JS が読み書きする。
大きなデータと構造化された検索が必要になって、非同期でトランザクションを持つ IndexedDB が入ります。4つ並んでいるのは、用途が増えるたびに足されたからで、上位互換の関係にはありません。
2つの軸に整理する
歴史を追うと4つに見えますが、判断のときは2つの軸で見ます。
軸1: JS から読めるか。 読めるなら、XSS が起きた時点で持ち出されます。 軸2: 自動で送られるか。 送られるなら、CSRF の対象になり、通信量にも乗ります。
この2軸は独立しています。「安全なストレージ」という1次元の順位は存在せず、置きたいものごとに、どちらの性質が要るかを選ぶことになります。
HttpOnly Cookie でセッションを管理しているサイトに XSS があります。攻撃者にできないことはどれですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この条件で、認証情報をどこに置きますか?
- 社内向け SPA。API は同じ親ドメインの別サブドメインにある
- 解約や権限剥奪を、遅くとも数分以内に反映したい
- サードパーティのスクリプトを1本入れる予定がある(計測タグ)
- 利用者は同じアプリを複数タブで開く
- 認証基盤は自前ではなく、外部の ID プロバイダを使う
- · XSS が起きたときに、何が持ち出されるか
- · 剥奪してから実際に使えなくなるまでの時間
- · 複数タブで状態がずれたときに何が起きるか
「トークンは localStorage でいいですよね」と聞いてきた同僚に、何を確認すべきか説明してください
- 相手は XSS という言葉は知っているが、起きたあと何が違うかは考えたことがない
- そのプロジェクトは外部のスクリプトを何本か読み込んでいる
- 頭ごなしに否定せず、判断できる材料を渡したい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。