メインコンテンツへスキップ
戻る

Local Storage、Session Storage、Cookies

ブラウザストレージの実践ガイド——トレードオフと、本番環境で重要なセキュリティ境界

ブラウザストレージは、認証フロー、マルチタブ体験、スキーマ変更に耐えなければならないアプリケーションの一部になるまで、単純に見えます。難しいのは API ではありません。本当の判断は 誰がデータを必要とするか、どれくらいの期間保持すべきか、どのセキュリティ境界を越えるか です。


用語の補足:ブラウザには cookieStorage という標準 API はありません。document.cookie による従来の cookies と、新しい非同期 Cookie Store APIcookieStore 経由)があります。どちらも同じ cookies を管理しますが、Web Storage のように振る舞うものではありません。


概要

PropertylocalStoragesessionStorageCookies
Typical lifetimeUntil explicitly cleared or evictedCurrent tab's page sessionSession-based or until their expiry date
Sent to serverNoNoYes, on matching requests
JavaScript accessYes, synchronouslyYes, synchronouslyYes, unless marked HttpOnly
Best suited forPersistent, non-sensitive preferencesTemporary state that should survive reloadsServer-managed sessions and small server-readable values

実務上の違いは、Web Storage がクライアント側に属し、cookies が HTTP リクエストのライフサイクルに参加する点です。どちらも信頼できる database として扱うべきではありません。



localStorage

localStorage は、ページのリロードやブラウザの再起動をまたいで、origin ごとにデータを永続化します。テーマ、非表示にした通知、未完了のローカル下書きなど、少量の非機密な preferences に向いています。


const preferences = {
  theme: "dark",
  density: "compact",
}

localStorage.setItem("preferences:v1", JSON.stringify(preferences))

const stored = localStorage.getItem("preferences:v1")
const parsed = stored ? JSON.parse(stored) : null

便利さには限界があります。値は文字列、access は同期的で、read や write のたびに main thread をブロックします。容量や eviction の挙動もブラウザによって異なります。database ではなく、小さな persistence 機構として扱っています。


その origin 上で動く JavaScript はすべて読み取れます。XSS 脆弱性経由で混入した悪意あるコードも含まれます。そのため session tokens、passwords、機微な個人データは localStorage に保存しません。長寿命の bearer tokens は特に危険で、値を盗むだけで他の場所で再利用できます。


stored keys に version を付け、parsed data を検証します。デプロイ済みのアプリは変わりますが、古い browser data は残ります:


type Preferences = {
  theme: "light" | "dark"
}

function readPreferences(): Preferences | null {
  try {
    const value = JSON.parse(localStorage.getItem("preferences:v1") ?? "null")

    if (value?.theme === "light" || value?.theme === "dark") {
      return value
    }
  } catch {
    // Corrupt or manually edited storage should not break the application.
  }

  return null
}


sessionStorage

sessionStorage は似た文字列ベースの API ですが、lifetime は page session に紐づきます。同一タブ内の reload には耐え、通常はそのタブや window を閉じると消えます。タブごとに storage は独立しています。


そのため、一時的な UI state に向いています。多段階フォーム、return URL、誤った refresh には耐えるが永続 preference にはならない filters などです。


sessionStorage.setItem(
  "checkout:draft",
  JSON.stringify({ step: 2, deliveryMethod: "pickup" })
)

セキュリティ境界ではありません。ページ上の scripts からも読めますし、ユーザーがタブを複製・復元する方法によって lifecycle の前提が崩れることもあります。sessionStorage を使う理由は product lifetime がデータと一致するからであり、localStorage より安全だからではありません。



Cookies は、ブラウザが HTTP リクエストに付与できる点が異なります。サーバーが値を必要とする場合、特に server-managed sessions に適しています。


セキュリティ上重要な cookies は、通常サーバー側で作成すべきです:


Set-Cookie: session=opaque-value; Path=/; HttpOnly; Secure; SameSite=Lax

HttpOnly は JavaScript による cookie の読み取りを防ぎ、Secure は HTTPS に限定し、SameSite は cross-site request forgery を抑えるのに役立ちます。これらの attributes はリスクを下げますが、output escaping、Content Security Policy、CSRF 分析、適切な session expiration と rotation の代わりにはなりません。


従来の document.cookie は同期的で、パースも扱いにくいです。Cookie Store API は、対応環境では非同期 interface を提供します:


const preference = await cookieStore.get("theme")

await cookieStore.set({
  name: "theme",
  value: "dark",
  path: "/",
  sameSite: "lax",
})

Client-side code は cookieStore 経由でも HttpOnly cookie を読めません。その制限が意図です。一般的な application state を cookies に入れることも避けます。サイズが小さく、domain と path のルールが微妙で、リクエストに含まれる cookies は毎回ネットワーク overhead を増やします。



私が使う判断基準

デフォルトの選択は次のとおりです:

  • データが現在開いているページの間だけ必要なら in-memory state
  • 一時 state が単一タブ内の reload に耐えるべきなら sessionStorage
  • 訪問をまたいで残る少量の非機密 preferences なら localStorage
  • サーバーが値を受け取る必要がある場合、特に secure な HTTP-only cookie 内の opaque session identifier なら cookies
  • 大量データ、structured records、transactions、offline 動作が必要なら IndexedDB

原則は、最短の有用 lifetime で、最小限のデータを保存することです。Browser storage はユーザーが制御でき、消去・改変され得ます。authoritative な source of truth として扱うべきではありません。読み取り時に values を検証し、storage failures を処理し、server-side authorization はクライアントが編集できるものに依存させないでください。


ストレージの選択は、最終的には API の好みではなく architecture の判断です。データの lifetime と trust level から始めれば、適切な browser primitive はたいてい自然についてきます。

次のノートを読む
JavaScript コア概念