mybook

第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
end

Surrogate-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
end

Cache-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 で分けない
RedisACL + bind + TLS + 危険コマンド無効化
パスglob ルーティング廃止、拡張子ベース無条件キャッシュ禁止

マナミの対応

セキュリティチームの指摘を受けて、マナミは多層防御を実装した:

  1. 認証コントローラを**デフォルト no-store**に(オプトイン方式)
  2. CDN で Surrogate-Control を活用し、ブラウザと CDN のキャッシュを分離
  3. Redis を VPC 内に移動、ACL で最小権限化、TLS 有効化
  4. glob ルーティングを廃止、想定外パスは 404

「キャッシュのセキュリティは『この応答は、誰に使い回されてよいのか?』を常に問い続けることだ」

次の章では、キャッシュの運用・監視について学ぶ。