Back

キャッシュしてはいけないもの

キャッシュしてはいけないもの

キャッシュはフロントエンドパフォーマンスにおける最強のツールの一つです。正しく使えば、冗長なネットワークリクエストを排除し、サーバー負荷を軽減し、アプリを瞬時に応答させることができます。しかし誤って使えば、プライベートデータが漏洩したり、認証済みコンテンツが古い状態で配信されたり、ユーザーが抜け出せない壊れたページ状態に閉じ込められたりします。

ルールは多くの記事が伝えるよりもシンプルです:静的でバージョン管理されたパブリックアセットは、ほぼ常に積極的なキャッシュが安全です。それ以外はすべて、デフォルトで安全でないものとして扱うべきです。

何をキャッシュすべきでないかを説明する前に、どこでキャッシュが行われるかを正確に理解しておくことが重要です。各レイヤーは動作が大きく異なるからです。

重要なポイント

  • キャッシュレイヤー(HTTP、CDN、Service Worker、bfcache、Web Storage)はそれぞれスコープとセキュリティ特性が異なります。これらを同一視すると実際のバグが発生します。
  • no-store はストレージへの保存を完全に防ぎますが、no-cache は再利用前の再検証を強制するだけです。
  • 認証済みまたはユーザー固有のレスポンスは、共有キャッシュがユーザー間でデータを漏洩させないよう、Cache-Control: private, no-store を使用する必要があります。
  • JWT、リフレッシュトークン、APIキーを localStorageIndexedDB に保存してはいけません。HttpOnly クッキーを優先してください。
  • 古いコピーや共有コピーがセキュリティや正確性の問題を引き起こす可能性がある場合は、デフォルトで no-store を使用してください。

キャッシュレイヤーは互換性がない

キャッシュに関するほとんどのミスは、以下のものを同一視することから始まります:

  • HTTP/ブラウザキャッシュRFC 9111 に基づき、Cache-Control ヘッダーによってリクエストごとに制御されます。タブ間で共有され、セッションをまたいで持続します。
  • CDN/共有キャッシュ — ユーザーとオリジンサーバーの間に位置します。Cloudflare CDN などのサービスは、一人のユーザーではなくすべてのユーザーに対してレスポンスをキャッシュします。
  • Service Worker Cache API — JavaScript で完全に制御できるプログラマブルキャッシュです。明示的にクリアするまで持続します。
  • bfcache(戻る/進むキャッシュ) — ブラウザがページ全体のインメモリスナップショットを保持し、ナビゲーションの「戻る」操作時に使用します。HTTPキャッシュとは別物です。
  • localStorage / sessionStorage — キーバリューストレージです。有効期限の管理機能がなく、JavaScriptから完全にアクセス可能です。
  • IndexedDB — 構造化された永続ストレージです。localStorage と同じXSSの攻撃対象領域を持ちます。

それぞれ永続性、スコープ、セキュリティ特性が異なります。これらを混同すると実際のバグが発生します。

Cache-Control のセマンティクスについて

この違いは広く誤って伝えられているため、明確に述べておく価値があります:

  • no-store は、キャッシュがレスポンスをまったく保存してはならないことを意味します。
  • no-cache は「キャッシュしない」という意味ではありません。再利用前にサーバーでキャッシュされたコピーを再検証しなければならないことを意味します。

レスポンスをどこにも保存させたくない場合は no-store を使用してください。条件付きリクエスト(ETag、304レスポンス)の効率性を維持しながら、毎回の使用時に鮮度を保証したい場合は no-cache を使用してください。

キャッシュしてはいけないもの

認証済みAPIレスポンスとユーザー固有のHTML

ユーザーのアイデンティティによって変化するレスポンス(ダッシュボードのHTML、アカウントページ、プロフィールデータを含むAPIレスポンスなど)は、共有キャッシュにキャッシュすべきではありません。一般的なパターンは以下の通りです:

Cache-Control: private, no-store

no-store は通常のHTTPキャッシュがレスポンスを保存することを防ぎ、private はCDNなどの共有キャッシュが複数のユーザー向けにキャッシュしないよう明示的に指示します。

private がない場合、Cloudflare などのCDNはユーザーAに返されたレスポンスをキャッシュし、ユーザーBに提供する可能性があります。これは実際のデータ漏洩インシデントを引き起こしてきました。

認証が必要なService Worker内のあらゆるもの

Service Worker Cache API は実質的にプログラマブルなネットワークプロキシです。Service Workerはそのスコープ内のすべてのfetchリクエストをインターセプトします。認証済みAPIレスポンスやユーザー固有のHTMLを無差別にキャッシュすると、そのデータはCache APIストレージに永続化されます。セッションをまたいで保持される可能性があり、ログイン状態に関わらずService Workerからアクセス可能になります。

// ❌ このようにしてはいけない
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request).then(response => {
      return caches.open('v1').then(cache => {
        cache.put(event.request, response.clone());
        return response;
      });
    })
  );
});

このパターンはすべてを無差別にキャッシュします。認証が必要なルートは明示的に除外してください:

// ✅ パブリックでバージョン管理されたアセットのみキャッシュする
const CACHEABLE = ['/shell.html', '/app-abc123.js', '/styles-f9c.css'];

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  if (url.origin !== self.location.origin) return; // クロスオリジンは無視
  if (!CACHEABLE.includes(url.pathname)) return;   // ネットワークに通す
  event.respondWith(
    caches.match(event.request).then(r => r || fetch(event.request))
  );
});

localStorage や IndexedDB に保存されたシークレット

OWASP HTML5セキュリティチートシート は明確に述べています:JWT、リフレッシュトークン、APIキー、パスワードリセットトークンを localStorageIndexedDB に保存することはリスクを伴います。なぜなら、ドメイン上のXSS脆弱性があればそれらを読み取ることができるからです。これらのストレージメカニズムには組み込みの有効期限がなく、HttpOnlyに相当するものもなく、注入されたスクリプトからの分離もありません。

可能な限り、トークンには HttpOnlySecureSameSite クッキーを使用してください。localStorage を使用しなければならない場合は、そのリスクを受け入れていることを理解した上で使用してください。

CDNレイヤーでのユーザー固有のレスポンス

よくあるCloudflare CDNのミス:ユーザーデータを含むレスポンスに private ディレクティブを付け忘れること、または Vary の設定ミスです。

# ❌ privateがない — CDNがすべてのユーザー向けにキャッシュする可能性がある
Cache-Control: max-age=300
Content-Type: application/json

# ✅ 正しい設定
Cache-Control: private, no-store

レスポンスが正当にリクエストヘッダー(Accept-LanguageAuthorization など)によって変化する場合、正しい Vary ヘッダーを含める必要があります。これがないと、CDNはフランス語のキャッシュされたレスポンスを英語圏のユーザーに提供したり、さらに悪い場合には認証済みのキャッシュされたレスポンスを未認証のユーザーに提供したりする可能性があります。

また、キーなしパラメータによるキャッシュポイズニングにも注意してください:CDNがURLに基づいてキャッシュするが、アプリがレスポンスの構築に未検証のクエリパラメータやヘッダーを読み取る場合、攻撃者は細工したリクエストでキャッシュを汚染できます。

bfcache で復元されるべきでないページ

Chromiumベースのブラウザは最近のバージョンで、Cache-Control: no-store を送信するページでもbfcacheを使用するようになっています。この動作はブラウザ依存であり、Chrome、Firefox、Safariで一貫していません。caniuse によると、bfcacheは最新のブラウザで幅広くサポートされていますが、no-store との相互作用はブラウザエンジンやバージョンによって異なります。

古いインメモリスナップショットの復元が本当に安全でないページ(一回限りの支払い確認を表示するページやセッションに依存した状態など)がある場合は、pageshow イベントを使用してbfcacheからの復元を検出し、リロードまたは再検証を行ってください:

window.addEventListener('pageshow', event => {
  if (event.persisted) {
    // ページがbfcacheから復元された
    window.location.reload();
  }
});

no-store がすべてのブラウザでbfcacheを無効にすると思い込まないでください。

デフォルトの考え方

何かをキャッシュすべきかどうか迷ったら、こう自問してください:古いコピーや共有コピーを提供することで、セキュリティや正確性の問題が発生する可能性があるか? もしそうなら、デフォルトで Cache-Control: no-store を使用し、そこから逆算して考えてください。

コンテンツハッシュされたURLを持つ静的アセット(JavaScriptバンドル、CSSファイル、画像など)は、max-age=31536000, immutable で1年間キャッシュしても安全です。ユーザーセッション、認証状態、または機密性の高い操作に紐づくものはすべて安全ではありません。

まとめ

積極的なキャッシュは機能です。誤ったものを偶発的にキャッシュすることは脆弱性です。重要なのは、どのレイヤーを対象としているか、各 Cache-Control ディレクティブが実際に何を保証するか、そしてどのカテゴリのデータがオリジンの厳格な管理下から決して離れてはならないかを把握することです。ユーザー、セッション、またはシークレットに紐づくものはすべてデフォルトでキャッシュ不可として扱い、積極的なキャッシュは本来の目的である静的でバージョン管理されたアセットのためにのみ使用してください。

よくある質問

可能ではありますが、推奨されません。ドメイン上にXSS脆弱性があれば、攻撃者はlocalStorageへの完全な読み取りアクセスを得ることができ、トークンを盗んでユーザーになりすますことができます。HttpOnly、Secure、SameSiteクッキーはJavaScriptから直接読み取ることができないため、より安全です。localStorageを使用しなければならない場合は、XSSリスクを受け入れていることになり、CSPと入力サニタイゼーションに多大な投資をすべきです。

no-storeは通常のHTTPキャッシュが再利用のためにレスポンスを保存することを禁止します。no-cacheはレスポンスの保存を許可しますが、再利用のたびにオリジンサーバーへの再検証を要求します。通常はETagまたはLast-Modifiedを使った条件付きリクエストを通じて行われます。機密データにはno-storeを使用し、鮮度の保証が必要だが304 Not Modifiedレスポンスの恩恵も受けたい場合はno-cacheを使用してください。

通常、レスポンスにprivateディレクティブがないか、Varyヘッダーが正しくないためです。privateがない場合、共有キャッシュはレスポンスを全員に対してキャッシュ可能なものとして扱います。AuthorizationやCookieなどのヘッダーに対するVaryがない場合、CDNはキャッシュキーの生成時にそれらのヘッダーを無視し、ユーザーAのレスポンスをユーザーBに提供する可能性があります。ユーザー固有のレスポンスには必ずCache-Control: private, no-storeを送信してください。

bfcacheを無効にする信頼性の高いクロスブラウザのヘッダーはありません。Cache-Control: no-storeは一部のブラウザでは機能しますが、一貫性がありません。信頼性の高いアプローチは、pageshoweventをリッスンしてevent.persistedを確認することです。trueの場合、ページはbfcacheから復元されており、強制的にリロードするか機密な状態を再取得することができます。これは支払い確認ページやログアウト後のページには不可欠です。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay