Webサイトの表示速度を上げるために欠かせない「キャッシュ」という仕組み。しかし、Cache-Control(キャッシュコントロール)の設定を間違えると、ログイン後のマイページや個人情報が別のユーザーに表示されてしまう重大なセキュリティインシデントを引き起こしかねません。
「no-cacheを設定しているから大丈夫」と思っている方は要注意です。no-cacheは「キャッシュを禁止する」設定ではなく、「使用前に再検証する」という意味で、CDNやプロキシサーバーへの保存は防げません。この誤解は実際の情報漏洩事故の原因にもなっています。
本記事では、Cache-Controlの基本概念・主要ディレクティブの正確な意味・セキュリティリスク・推奨設定パターンまで、最新の仕様(RFC 9111)をもとに体系的に解説します。
Cache-Controlとは?基本概念と役割
Cache-Controlとは、ブラウザやCDN、プロキシサーバーなどがWebコンテンツをどのようにキャッシュするか(保存するか・保存しないか・どのくらいの期間保存するか)を指定するHTTPヘッダーです。RFC 9111(HTTP Caching)で定義されており、HTTPレスポンスとHTTPリクエストの両方で利用できます。
Webブラウザはキャッシュ機能を使うことで、一度取得したリソース(HTML、画像、CSS、JavaScriptなど)をユーザーのデバイス上に保存し、次回アクセス時に再ダウンロードせずに表示します。これにより表示速度が向上し、サーバーへの負荷が下がるというメリットがあります。しかし、キャッシュの制御を誤ると、個人情報や機密データが不適切に保存・共有されるセキュリティリスクが生じます。
Cache-Controlヘッダーは、レスポンスに付与してブラウザ・CDN・プロキシへ指示を出す「レスポンス側の設定」と、リクエストに付与してキャッシュサーバーへの要求を伝える「リクエスト側の設定」の2種類があります。Webサーバーの設定として問題になるのは主にレスポンス側の設定です。
Webキャッシュの種類(プライベートキャッシュ・共有キャッシュ)
Cache-Controlを正しく理解するには、「どこがキャッシュするのか」を把握することが重要です。Webキャッシュは大きく2種類に分かれます。
プライベートキャッシュ(ブラウザキャッシュ)
ユーザーの端末上のブラウザに保存されるキャッシュです。そのユーザーのみがアクセスできます。ログイン後に表示されるマイページのHTMLがブラウザにキャッシュされた場合でも、そのデバイスのそのユーザーしか見られないため、比較的安全です。
共有キャッシュ(CDN・プロキシキャッシュ)
CDN(コンテンツデリバリネットワーク)や中間プロキシサーバーに保存されるキャッシュです。複数のユーザーが共有するため、個人情報が含まれるコンテンツが誤って保存されると、別のユーザーにその情報が表示されてしまう危険があります。
セキュリティの観点で特に重要なのは共有キャッシュへの保存をどう制御するかです。CDNを利用するWebサービスでは、Cache-Controlの設定ミスが直接、個人情報漏洩につながるリスクがあります。
主要ディレクティブ一覧と意味
Cache-Controlヘッダーは「ディレクティブ」と呼ばれる指示の組み合わせで構成されます。複数のディレクティブをカンマ区切りで指定できます(例:Cache-Control: no-cache, no-store, private)。

キャッシュ保存の制御
- no-store:あらゆるキャッシュ(プライベート・共有)にコンテンツを保存させない。最も強力な設定。
- no-cache:キャッシュへの保存は許可するが、使用前にオリジンサーバーで再検証することを要求する。「キャッシュを禁止する」設定ではない点に注意。
- private:ブラウザ(プライベートキャッシュ)にのみ保存を許可する。CDN・プロキシへの保存を防ぐ。
- public:共有キャッシュを含む、あらゆるキャッシュへの保存を許可する。
有効期間の設定
- max-age=秒数:レスポンスが生成されてからキャッシュが有効な秒数を指定する。例:
max-age=3600は1時間。 - s-maxage=秒数:共有キャッシュ(CDNなど)のみに適用されるキャッシュ有効期間。
max-ageよりも優先される。 - immutable:キャッシュ有効期間中は内容が変更されないため、再検証を不要とする(RFC 8246)。ハッシュ付きファイル名の静的リソースに適用する。
再検証の制御
- must-revalidate:キャッシュの有効期限切れ後は、オリジンサーバーへの再検証なしに古いキャッシュを使用することを禁止する。オリジンへの到達が不能な場合は504エラーを返す。
- proxy-revalidate:
must-revalidateの共有キャッシュ版。 - stale-while-revalidate=秒数:バックグラウンドで再検証しながら、指定秒数の間は古いキャッシュを返す(RFC 5861)。
- stale-if-error=秒数:オリジンエラー時に、指定秒数の間は古いキャッシュを返す(RFC 5861)。
その他
- no-transform:CDNや中間装置による画像の自動圧縮・変換を禁止する。
- must-understand:キャッシュがレスポンスのステータスコードのキャッシュ要件を理解している場合のみ保存を許可する。
no-storeと組み合わせて使用する。
no-cacheとno-storeの違い
Cache-Controlの設定ミスの中で最も多く、かつ深刻なのがno-cacheとno-storeの混同です。名前が似ているため「両方ともキャッシュを禁止する設定」と誤解されがちですが、動作はまったく異なります。
no-cacheの正しい意味
no-cacheは「キャッシュしてはいけない」という意味ではありません。正確には「キャッシュに保存することは許可するが、再利用する前に必ずオリジンサーバーに問い合わせて有効性を確認(再検証)すること」という指示です。再検証を経た上で問題なければ、キャッシュを使って応答を返します。
つまりno-cacheを設定しても、CDNやプロキシにコンテンツが保存される可能性は残ります。再検証の仕組み(ETagやLast-Modified)を使ってキャッシュを更新管理したい場合に用いるディレクティブです。
no-storeの正しい意味
no-storeは「プライベートキャッシュ・共有キャッシュを問わず、いかなるキャッシュにも保存させない」という最も強い制限です。銀行口座情報や医療情報など、絶対にキャッシュさせてはいけないコンテンツに使用します。
競合するディレクティブが同時に指定された場合、最も制限の強いものが優先されます(MDN Web Docs / RFC 9111)。たとえばprivate, no-cache, no-store, max-age=0, must-revalidateと指定した場合、実質的にはno-storeと同等の効果になります。
public・privateディレクティブの違い
publicとprivateはどのキャッシュに保存を許可するかを制御するディレクティブです。
privateとは
privateを指定すると、ブラウザ(プライベートキャッシュ)のみへの保存を許可し、CDNやプロキシなどの共有キャッシュへの保存を禁止します。ログイン後に取得するユーザー固有のページには必ず付与すべきディレクティブです。
MDN Web Docsは「ログイン後に受け取るレスポンスにはprivateを追加すべきです。付け忘れると個人情報漏えいにつながります」と明記しています(最終更新:2026年9月27日)。
publicとは
publicを指定すると、共有キャッシュを含むあらゆるキャッシュへの保存を許可します。誰もが見てよいコンテンツ(トップページの画像、共通のCSSなど)に適しています。
Authorizationヘッダーを含むリクエストへの応答にpublicを指定すると、認証済みの応答が共有キャッシュに保存されてしまいます。意図的でない限り、認証付きリクエストへの応答にpublicを使わないようにしてください(RFC 9111 / MDN Web Docs)。設定ミスが招くセキュリティリスク
Cache-Controlの設定ミスは、次のような深刻なセキュリティリスクに直結します。
個人情報・機密情報のCDN漏洩
ログイン後のマイページや購入履歴ページなどにprivateまたはno-storeが設定されていない場合、そのコンテンツがCDNや中間プロキシにキャッシュされる可能性があります。別のユーザーが同じURLにアクセスした際に、他人の個人情報が表示されてしまう重大な情報漏洩につながります。
no-cacheの誤用による意図しないキャッシュ
「no-cacheを設定しているから安全」と考えてログインページや決済ページにno-cacheのみを設定している場合でも、CDNに応答が保存されてしまう可能性があります。no-cacheはキャッシュを禁止するのではなく、「再検証してから使うよう指示する」ディレクティブです。キャッシュへの保存そのものを防ぐにはno-storeが必要です。
Requestコラプシングによる情報共有
CDNには「リクエストコラプシング(Request Collapsing)」という最適化機能があります。これは、同一URLへの同時リクエストをまとめて1回だけオリジンに問い合わせ、結果を複数のリクエストで共有する動作です。
RFC 9111第4節でも認められたキャッシュの最適化ですが、適切な設定がなされていないと、ユーザーAの個人情報を含むレスポンスがユーザーBにも返されてしまうリスクがあります。
ヒューリスティックキャッシュ
Cache-Controlヘッダーが付与されていないレスポンスに対して、ブラウザは独自の推測(ヒューリスティック)でキャッシュ期間を設定することがあります。
WebサーバーがLast-Modifiedヘッダーを返している場合、その経過時間の約10%をキャッシュ期間として設定するブラウザの動作が確認されています(RFC 7234)。意図しないキャッシュを防ぐために、Cache-Controlは必ず明示的に設定することが推奨されます。
機密ページへのキャッシュによる脆弱性の残留
古いバージョンのページがキャッシュに残っていると、脆弱性を修正したはずのページの古いバージョンが引き続き使われる可能性があります。適切な再検証の仕組みがないまま長期キャッシュを設定すると、セキュリティパッチが有効にならない期間が生じます。
実際の情報漏洩事例
Cache-Controlの設定ミスが引き起こした情報漏洩事故として、国内で広く知られているのが2017年のメルカリの事例です。
- 概要:2017年6月、フリマアプリ「メルカリ」において、一部ユーザーが他のユーザーの個人情報(氏名・住所・電話番号など)を閲覧できる状態になっていたことが判明しました。
- 原因:個人情報を含むページのCache-Controlに
no-storeではなくno-cacheのみが設定されていたこと、そしてCDNのリクエストコラプシング(同時リクエストの最適化)機能との組み合わせにより、本来表示されるべきでないユーザーの情報が別のユーザーに返されたとされています。 - 教訓:CDNを利用するサービスでは、
no-cacheとno-storeの違いを正確に理解した上でCache-Controlを設定する必要があります。個人情報を含むページにはno-storeおよびprivateを設定することが安全策です。
このような事例からわかるとおり、Cache-Controlは「なんとなく設定する」のではなく、各ディレクティブの正確な意味と、CDNの動作を理解した上で設計する必要があります。
機密情報ページの正しいCache-Control設定
ログイン後のマイページ、決済ページ、個人情報確認ページなど、ユーザー固有の情報を含むページには次の設定が推奨されます。
機密情報ページの推奨設定
Cache-Control: no-store, private
または、よりフォールバックを考慮した設定
Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
各ディレクティブの役割は次のとおりです。
- no-store:あらゆるキャッシュへの保存を禁止する(最重要)
- private:共有キャッシュへの保存を禁止し、ブラウザのみに限定する
- no-cache:保存された場合でも、使用前に再検証を要求する(二重の安全策)
- max-age=0:キャッシュの有効期間をゼロにする
- must-revalidate:有効期限切れ後はオリジンサーバーへの再検証を必須とする
private, no-cache, no-store, max-age=0, must-revalidateは冗長に見えますが、CDNやブラウザごとにCache-Controlの解釈に微妙な差があるため、複数のディレクティブを組み合わせることで安全性を高める実践的なアプローチです(Qiita「Cache-Controlヘッダとキャッシュを防ぐ方法」2025年更新版)。実際にZennも個別記事ページにこの組み合わせを採用しています。
静的リソースの最適な設定パターン
セキュリティ上の問題がなく、誰でも閲覧できる静的リソース(画像、CSS、JavaScriptなど)については、積極的にキャッシュを活用して表示速度を向上させることが推奨されます。ポイントはファイルの「更新頻度と識別方法」で設定を使い分けることです。
ハッシュ付き静的リソース(最も安全なキャッシュ戦略)
ビルドツール(webpack、Viteなど)によってファイル名にコンテンツハッシュが付与されたリソース(例:app.a1b2c3d4.js)は、ファイルの内容が変わるたびにURLが変わるため、長期キャッシュが安全に使えます。
推奨設定:Cache-Control: public, max-age=31536000, immutable
- public:CDNを含むあらゆるキャッシュへの保存を許可
- max-age=31536000:1年間(31,536,000秒)キャッシュを有効にする
- immutable:有効期間中は内容が変わらないため、再検証を不要とする(RFC 8246)
HTMLファイル(index.html・キャッシュバスト対象)
アプリケーションのエントリポイントとなるHTMLは、中身が更新されたら即時反映させる必要があります。ハッシュが付かないHTMLにはno-cacheを設定し、常に最新版を確認させます。
推奨設定:Cache-Control: no-cache キャッシュへの保存は許可しつつ、必ずオリジンサーバーで最新性を確認してから提供します。
頻繁に更新される動的コンテンツ(トップページなど)
内容は変わるが短時間のCDNキャッシュを許容できるページには、ブラウザには即時再検証させながらCDNには短時間キャッシュを許可する設定が有効です。
推奨設定:Cache-Control: public, max-age=0, s-maxage=900
- ブラウザ側(
max-age=0):毎回再検証 - CDN側(
s-maxage=900):15分間キャッシュ有効 (参考:Zennトップページの実際の設定)
可用性を重視したキャッシュ設定
オリジンサーバーの障害時にもサービスを継続させたい場合は、stale-while-revalidateやstale-if-errorの活用が有効です。
例:Cache-Control: max-age=604800, stale-while-revalidate=86400 7日間キャッシュし、バックグラウンドで再検証しながら最大1日間は古いキャッシュを返します。
例:Cache-Control: max-age=604800, stale-if-error=86400 オリジンエラー時に最大1日間は古いキャッシュで応答します。
CDN利用時の注意点
CDNを利用する場合、Cache-Controlの設定はオリジンサーバーとCDNの両方で考慮が必要です。CDNは必ずしもオリジンのCache-Controlをそのまま従うわけではありません。
CDN独自のキャッシュルール
CDNによってはオリジンサーバーのCache-Controlよりも管理画面の設定を優先したり、ファイルの拡張子で静的ファイルかどうかを自動判断したりします。たとえばJ-Stream CDNextでは、優先順位が次のように定義されています。
no-cache = no-store = private = Expires:-1 > s-maxage > max-age > Expires > Web管理画面の設定
これはJ-Stream CDNextの仕様であり、他のCDNには適用されません。使用するCDNのドキュメントを必ず確認してください。
Cloudflareの場合は、デフォルトで拡張子から静的ファイルを判断してキャッシュします。Cache-Controlを明示的に設定しておかないと意図しない動作になる場合があります。AWS CloudFrontのCachingOptimizedポリシーはデフォルトで24時間のキャッシュを設定します。
Varyヘッダーとの組み合わせ
Varyヘッダーは、キャッシュキーに含めるリクエストヘッダーを指定します(例:Vary: Accept-Encoding)。しかしCDNによってはVaryに指定されたヘッダーが限られることがあります(AkamaiはデフォルトでAccept-Encoding以外を無視するなど)。Varyを使う場合は使用するCDNの対応状況を事前に確認してください。
リクエスト側Cache-ControlはCDNに無視される
ブラウザが強制再読み込みのときに付与するCache-Control: no-cache(リクエスト側)は、CDNは通常無視します。CDNのキャッシュを完全にクリアするには、CDN側のキャッシュパージ機能を使う必要があります。
Apache・Nginxへの設定方法

Apacheでの設定例
mod_headersモジュールを使って.htaccessまたはhttpd.confに記述します。
機密情報ページ・ログイン後のページ
<Location /mypage>
Header set Cache-Control "no-store, private"
</Location>
ハッシュ付き静的アセット(JS・CSS・画像)
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|jpg|jpeg|png|gif|ico|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
</IfModule>
HTMLファイル(キャッシュバスト対象)
<FilesMatch "\.html$">
Header set Cache-Control "no-cache"
</FilesMatch>
Nginxでの設定例
Nginxではadd_headerまたはexpiresディレクティブを使って設定します。
機密情報ページ
location /mypage {
add_header Cache-Control "no-store, private" always;
}
ハッシュ付き静的リソース
location ~* \.(css|js|jpg|jpeg|png|gif|ico|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
HTMLファイル
location ~* \.html$ {
add_header Cache-Control "no-cache";
}
Nginxではadd_headerが親ブロックの設定を引き継がないという挙動があります。サブロケーション内でadd_headerを使う場合は、必要なヘッダーをすべて明示的に記述するか、alwaysパラメータの使用を検討してください。
実践チェックリスト
自社・自サービスのCache-Control設定状況を確認してください。
機密情報・ログイン後ページの確認
- ログイン後のマイページに
no-storeが設定されているか - 個人情報を含むレスポンスに
privateが付与されているか - 決済・購入確認ページに
no-store, privateが設定されているか Authorizationヘッダー付きリクエストの応答にpublicが誤設定されていないか
静的リソースの設定確認
- ハッシュ付きJS・CSSに
public, max-age=31536000, immutableが設定されているか - HTMLファイルに
no-cacheが設定され、最新版が確認できる状態になっているか - Cache-Controlが未設定のリソースが存在しないか(ヒューリスティックキャッシュのリスク)
CDN設定の確認
- 利用CDNのCache-Control優先順位(管理画面設定 vs オリジン設定)を把握しているか
- CDN側で機密ページのキャッシュが無効になっていることを確認したか
- CDNのリクエストコラプシング(Request Collapsing)機能が個人情報ページに影響しないか確認したか
動作確認と監査
- ブラウザの開発者ツール(DevTools)でレスポンスヘッダーにCache-Controlが正しく付与されているか確認したか
- ログイン後のページを別ユーザー(ログアウト状態)で確認し、他人の情報が表示されないことを確認したか
- Varyヘッダーを使用している場合、CDNが正しく対応しているか確認したか
よくある質問(FAQ)
Q. no-cacheだけ設定しておけば個人情報ページは安全ですか?
いいえ、安全ではありません。no-cacheはキャッシュへの保存を禁止するディレクティブではなく、「使用前にオリジンで再検証すること」を指示するものです。
CDNやプロキシにコンテンツが保存されたまま再検証されてしまうケースもあり、個人情報の漏洩につながるリスクがあります。機密情報ページには必ずno-store(およびprivate)を設定してください。
Q. Cache-Controlを設定していないページはどうなりますか?
Cache-Controlが設定されていない場合、ブラウザはヒューリスティック(経験則)に基づいて独自のキャッシュ期間を設定することがあります。サーバーがLast-Modifiedヘッダーを返している場合、ブラウザはその経過時間の約10%をキャッシュ期間として設定する場合があります(RFC 7234)。意図しないキャッシュを防ぐためにも、Cache-Controlは必ず明示的に設定してください。
Q. HTTPSを使っていればキャッシュによる情報漏洩は防げますか?
いいえ、HTTPSはあくまでも通信経路の暗号化です。CDNはHTTPSの通信を終端(SSL終端)してからオリジンサーバーと通信するため、Cache-Controlの設定ミスによるCDNへの保存は防げません。HTTPSとCache-Controlの設定は独立して対策が必要です。
Q. Expiresヘッダーとmax-ageが両方設定されている場合、どちらが優先される?
max-age(Cache-Controlの一部)がExpiresヘッダーよりも優先されます。Expiresは旧来のHTTP/1.0で使われていたヘッダーで、絶対日時で指定します。現在はCache-Controlのmax-ageの使用が推奨されています。両方設定されている場合はmax-ageが有効になります。
Q. ブラウザのキャッシュを確実にクリアさせる方法はありますか?
既存のキャッシュをサーバー側から強制的に削除する手段はありません(MDN Web Docsにも「既に中間キャッシュにある内容を消すディレクティブは存在しない」と明記されています)。
ブラウザのキャッシュのみであればClear-Site-Data: cacheヘッダーで消去できますが、CDNのキャッシュはCDN管理画面のキャッシュパージ機能を使う必要があります。新しいリリース時に古いキャッシュを使わせないためには、ファイル名にハッシュを付与するキャッシュバスト戦略が有効です。
Q. s-maxageとmax-ageは何が違いますか?
max-ageはプライベートキャッシュ(ブラウザ)と共有キャッシュ(CDNなど)の両方に適用されるキャッシュ有効期間です。一方s-maxageは共有キャッシュにのみ適用され、両方が指定された場合は共有キャッシュではs-maxageが優先されます。
ブラウザには長くキャッシュさせずにCDNには長くキャッシュさせたい、または逆の設定をしたい場合に両者を使い分けます。
まとめ
Cache-Controlは、Webサイトの表示速度向上とセキュリティ確保の両立において中心的な役割を果たすHTTPヘッダーです。最も重要なポイントは、no-cacheが「キャッシュを禁止する」設定ではなく「再検証を要求する」設定であるという点です。この誤解が実際の個人情報漏洩事故を引き起こした事例(メルカリ2017年)からも、ディレクティブの正確な意味の理解がいかに重要かがわかります。
ログイン後の個人情報ページや決済ページにはno-storeとprivateを必ず設定し、共有キャッシュへの保存を防ぐことが基本中の基本です。一方で、ハッシュ付きの静的リソースにはpublic, max-age=31536000, immutableで長期キャッシュを活用し、パフォーマンスと安全性を両立させることができます。CDNを利用する場合は、CDNごとのCache-Controlの解釈の違いやリクエストコラプシングの動作を把握した上で設計を行うことが欠かせません。
Cache-Controlの設定は、一度設定したら終わりではなく、CDNの変更・サービスの機能追加・フレームワークのアップデートのたびに見直す必要があります。本記事のチェックリストを活用して、定期的に設定の妥当性を検証する習慣を取り入れてみてください。

























![中小企業の情報瀬キィリティ相談窓口[30分無料]](/wp-content/uploads/2023/07/bnr_footer04.png)



no-cache ≠ キャッシュ禁止 no-store = キャッシュ保存の禁止