第9章 — キャッシュセキュリティ: 見えない脅威
「キャッシュは攻撃対象になる」
ある日、セキュリティチームからアラートが来た。「CDN キャッシュに不正なデータが混入している可能性がある」
ケンジは深刻な顔で言った。「キャッシュはパフォーマンスの味方だが、セキュリティの敵にもなる。3つの主要な攻撃を知っておく必要がある」
Cache Poisoning(キャッシュポイズニング)
攻撃者が悪意あるレスポンスをキャッシュに注入し、他のユーザーに配信させる攻撃。
Loading diagram...
対策
# 不要なヘッダーを無視
proxy_ignore_headers X-Forwarded-Host;
# Vary ヘッダーを適切に設定
add_header Vary "Accept-Encoding";
# キャッシュキーに含めるヘッダーを限定
proxy_cache_key "$scheme$request_method$host$uri$is_args$args";WARNING
キャッシュキーに含まれないリクエストヘッダーがレスポンスに影響する場合、Cache Poisoning のリスクがある。Vary ヘッダーで明示するか、そのヘッダーを無視すること。
Web Cache Deception(キャッシュ欺瞞)
攻撃者がユーザーの個人情報を CDN にキャッシュさせる攻撃。
Loading diagram...
対策
# 静的ファイル以外はキャッシュしない
location ~* \.(css|js|png|jpg)$ {
add_header Cache-Control "public, max-age=31536000";
}
location / {
add_header Cache-Control "private, no-store";
}- パスの正規化を厳密に行う
- Content-Type とファイル拡張子の整合性を検証
- 個人情報を含むレスポンスには
Cache-Control: private, no-store
Timing Attack(タイミング攻撃)
キャッシュのヒット/ミスの応答時間の差から情報を推測する攻撃。
Loading diagram...
対策
- 全レスポンスに一定の遅延を追加(不正確だが有効)
- キャッシュミス時も固定時間で返す
- 存在しないリソースもキャッシュする(ネガティブキャッシュ)
セキュリティチェックリスト
| 対策 | 実装 |
|---|---|
個人情報は private, no-store | ✅ |
| Cookie を含むレスポンスは CDN にキャッシュしない | ✅ |
| キャッシュキーに含めるヘッダーを明示 | ✅ |
| パスの正規化 | ✅ |
| 認証付きリクエストの Cache-Control 検証 | ✅ |
| Redis にパスワードを設定 | ✅ |
| Redis をプライベートネットワークに配置 | ✅ |
Rails での安全なキャッシュ設計
オプトイン方式(デフォルト no-store)
認証コントローラのデフォルトを no-store にし、公開ページだけ明示的に public を選ぶ。
class AuthenticatedController < ApplicationController
before_action :set_no_store
private
def set_no_store
response.headers["Cache-Control"] = "no-store"
response.headers["Pragma"] = "no-cache"
end
end
# 公開ページだけ上書き
class ArticlesController < ApplicationController
def show
@article = Article.published.find(params[:id])
expires_in 10.minutes, public: true,
"stale-while-revalidate": 60
end
endSurrogate-Control による層分離
Cache-Control はブラウザにも CDN にも届く。Surrogate-Control は CDN だけが解釈する。
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
response.headers["Cache-Control"] = "public, max-age=60"
response.headers["Surrogate-Control"] = "max-age=3600"
end
endCache-Control 判断フローチャート
Loading diagram...
Redis のセキュリティ
ACL による最小権限化(Redis 6+)
user default off
user app on >StrongPassword123 ~cache:* ~session:* +@read +@write -@dangerous -flushall -config -keys
user monitor on >MonitorPass +info +ping -@all
ネットワーク制限
# redis.conf
bind 127.0.0.1 10.0.1.5
protected-mode yes
- セキュリティグループでアプリサーバーのみ接続許可
- プライベートサブネットに配置、パブリック IP を持たせない
セキュアキャッシュ設計チェックリスト
| カテゴリ | チェック項目 |
|---|---|
| 機密データ | 決済・個人情報は no-store |
| ユーザー固有 | private または no-store |
| 認証 | デフォルト no-store、公開のみオプトイン |
| CDN | オリジンヘッダー尊重、Surrogate-Control 活用 |
| キャッシュキー | 正規化済み、Unkeyed Input が応答に影響しない |
| Vary | 応答を変えるヘッダーを明記、Cookie/Auth で分けない |
| Redis | ACL + bind + TLS + 危険コマンド無効化 |
| パス | glob ルーティング廃止、拡張子ベース無条件キャッシュ禁止 |
マナミの対応
セキュリティチームの指摘を受けて、マナミは多層防御を実装した:
- 認証コントローラを**デフォルト
no-store**に(オプトイン方式) - CDN で Surrogate-Control を活用し、ブラウザと CDN のキャッシュを分離
- Redis を VPC 内に移動、ACL で最小権限化、TLS 有効化
- glob ルーティングを廃止、想定外パスは 404
「キャッシュのセキュリティは『この応答は、誰に使い回されてよいのか?』を常に問い続けることだ」
次の章では、キャッシュの運用・監視について学ぶ。