?
← Case filescheckpoint 1 / 9 · ~17 min left
CASE-004INCIDENT17 MIN

The Silent Cache Poisoning Disaster: When CDN Becomes Your Enemy

Himanshu Kukrejareads
The Silent Cache Poisoning Disaster: When CDN Becomes Your Enemy
FIG. 1 · Blueprint · CASE-004
watch it first · newThe whole case in a 11-minute animated lecture3 quiz checkpoints · ask the AI about any moment · captions, chapters and speed

Lesson · 18 min

Understanding the Problem: When Privacy Meets Caching Gone Wrong#

Meet David Kim, Senior Backend Engineer at ShopFast, a rapidly growing e-commerce platform that's been preparing for their biggest Black Friday ever. Traffic is already hitting 10x normal volumes by 8 AM, and their infrastructure is holding steady. Dashboard shows all green, servers are humming, and David is feeling confident about their months of preparation.

Then at 10:47 AM, the support Slack channel explodes with increasingly alarming reports:

Support Team Messages:

text4 lines
Sarah_Support: Customer reporting they can see someone else's cart with $2,400 worth of items
Mike_Support: Multiple reports of users logged into wrong accounts - seeing different names/addresses
Jennifer_Support: URGENT: Customer can see partial credit card info (last 4 digits) for someone in Texas
David_Support: This is really bad - getting 20+ calls about privacy violations

David's heart sinks as he realizes they're facing every e-commerce company's worst nightmare: cross-customer data exposure during peak shopping season. The most terrifying part? All their backend monitoring shows everything is perfectly healthy.

The Crisis Numbers:

  • Traffic volume: 500,000 concurrent users
  • Affected users: ~15,000 customers across 12 edge locations
  • Data exposed: Shopping carts, profile info, addresses, partial payment data
  • Business impact: Potential GDPR violations, customer trust destruction
  • Time to resolution: Unknown - root cause still hidden

Our Mission: Uncover how a CDN configuration error turned private customer data into a shared cache, and build bulletproof defenses to ensure this never happens again.

ExplainerThe whole story: the CDN cache poisoning disaster

At 10:47 AM on Black Friday about 15,000 ShopFast customers saw strangers' carts while every backend dashboard was green; one curl showed cf-cache-status HIT and x-cache-hits 47. The CDN cached /api/* for 300 seconds keyed by URL path and ignored the origin's private, no-cache headers. The team purged, bypassed personal endpoints, added Vary, classified every endpoint and tested as two users: caching is a security boundary.

The Investigation: Following the Digital Breadcrumbs#

Initial Panic and False Leads#

David's First Instincts (All Wrong):

text17 lines
# Check database integrity
SELECT user_id, session_id, cart_items FROM user_carts 
WHERE created_at > '2025-11-28 08:00:00';
# Result: All data correct, proper user associations

# Check authentication service
grep "session_hijack\|auth_failure" /var/log/auth-service/*.log
# Result: No suspicious activity, all sessions properly validated

# Check load balancer sticky sessions
curl -v https://api.shopfast.com/api/user/cart \
  -H "Cookie: session_id=user123_session"
# Result: Correctly routes to same backend server

# Check application code for session bleeding
git log --since="1 week ago" --grep="session\|cache\|user"
# Result: No recent changes to session management

The Misdirection: Every backend system was working perfectly. The issue was invisible to traditional monitoring because it was happening at the edge layer - the CDN was serving cached responses without the backend ever knowing.

The Breakthrough Discovery#

David's "Aha!" Moment:

text14 lines
# Test the same API from different locations
curl -H "Authorization: Bearer user_123_token" \
     -H "X-Forwarded-For: 192.168.1.1" \
     https://api.shopfast.com/api/user/cart

# Response: User 456's cart (WRONG USER!)

# Check CDN cache headers
curl -I https://api.shopfast.com/api/user/cart
# Result revealed the smoking gun:
HTTP/2 200 
cache-control: public, max-age=300
cf-cache-status: HIT  # ← DISASTER! Private data served from cache
x-cache-hits: 47      # ← 47 different users got the same cart!

The Horror Realization: Their CDN was treating personalized API endpoints like static content, caching the first user's response and serving it to everyone who requested the same URL path.

DiagramHow one cart reached 47 users
%% title: How one cart reached 47 users
sequenceDiagram
    autonumber
    participant A as User A
    participant B as User B
    participant CDN as CDN edge
    participant API as api.shopfast.com
    A->>CDN: GET /api/user/cart (Bearer token_A)
    CDN->>API: cache miss · forward
    API-->>CDN: User A's cart
    Note over CDN: cache-control: public, max-age=300<br/>cache key = URL path only
    CDN-->>A: User A's cart
    B->>CDN: GET /api/user/cart (Bearer token_B)
    Note over CDN,API: same cache key · backend never asked
    CDN-->>B: User A's cart (cf-cache-status: HIT)
    Note over B,CDN: x-cache-hits: 47 · 47 different users got the same cart

Immediate Damage Control: The First 3 Actions#

Action 1: Purge All Cached Personal Data (0-5 minutes)#

text28 lines
# Emergency CDN cache purge - nuclear option
# Purge all potentially personalized endpoints

# CloudFlare API example
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
     -H "Authorization: Bearer ${CF_TOKEN}" \
     -H "Content-Type: application/json" \
     -d '{
       "purge_everything": true
     }'

# More targeted purge by URL prefix ("files" takes exact URLs only, no wildcards)
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
     -H "Authorization: Bearer ${CF_TOKEN}" \
     -H "Content-Type: application/json" \
     -d '{
       "prefixes": [
         "api.shopfast.com/api/user/",
         "api.shopfast.com/api/cart/",
         "api.shopfast.com/api/profile/",
         "api.shopfast.com/api/orders/"
       ]
     }'

# AWS CloudFront example
aws cloudfront create-invalidation \
    --distribution-id E1234567890123 \
    --paths "/api/user/*" "/api/cart/*" "/api/profile/*"

Action 2: Force All Personal Endpoints to Bypass CDN (5-10 minutes)#

text28 lines
// Emergency CDN bypass headers - add to all personal endpoints
const bypassCDNMiddleware = (req, res, next) => {
  // Nuclear option: prevent ANY caching
  res.set({
    'Cache-Control': 'no-store, no-cache, must-revalidate, private',
    'Pragma': 'no-cache',
    'Expires': '0',
    'Surrogate-Control': 'no-store',
    'CDN-Cache-Control': 'no-store',  // CDN-targeted (Cloudflare and others honour it)
    'X-Accel-Expires': '0'        // Nginx specific
  });
  next();
};

// Apply to all potentially personal endpoints
const personalEndpoints = [
  '/api/user/*',
  '/api/cart/*', 
  '/api/profile/*',
  '/api/orders/*',
  '/api/wishlist/*',
  '/api/addresses/*',
  '/api/payment-methods/*'
];

personalEndpoints.forEach(path => {
  app.use(path, bypassCDNMiddleware);
});

Action 3: Implement Session-Based Vary Headers (10-15 minutes)#

text19 lines
// More sophisticated approach: make cache key include user context
app.use('/api', (req, res, next) => {
  const isPersonalEndpoint = req.path.match(/\/(user|cart|profile|orders)/);
  
  if (isPersonalEndpoint) {
    // Make CDN treat each user as separate cache entry
    res.set({
      'Vary': 'Authorization, Cookie',
      'Cache-Control': 'private, no-cache',
      'X-User-Context': req.user?.id || 'anonymous'
    });
  }
  next();
});

// Note: the CDN computes its cache key from the *request*, before the origin
// answers, so a response header (e.g. X-Cache-Key) cannot change it. Put the
// session cookie / Authorization header into the key in the CDN's own config,
// and never echo a bearer token back in a response header.

Root Cause Analysis: How Private Data Became Public Cache#

The Misconfiguration Chain of Disasters#

Problem 1: Default CDN Rules

text20 lines
# CDN Configuration (WRONG - what they had)
cache_rules:
  - pattern: "/api/*"
    cache_ttl: 300  # 5 minutes
    cache_key: "url_path"  # ← DISASTER: ignores user context
    respect_origin_headers: false  # ← IGNORES backend cache headers!
    
# What it SHOULD have been:
cache_rules:
  - pattern: "/api/products/*"
    cache_ttl: 300
    cache_key: "url_path"
    
  - pattern: "/api/user/*"
    cache_ttl: 0  # NO CACHING
    bypass_cache: true
    
  - pattern: "/api/cart/*"  
    cache_ttl: 0  # NO CACHING
    bypass_cache: true

Problem 2: Backend Cache Headers Ignored

text10 lines
// Backend was sending correct headers (but CDN ignored them!)
app.get('/api/user/cart', authenticate, (req, res) => {
  res.set({
    'Cache-Control': 'private, no-cache',  // ← CDN ignored this!
    'Vary': 'Authorization'                // ← CDN ignored this too!
  });
  
  const cart = getUserCart(req.user.id);
  res.json(cart);
});

Problem 3: URL-Only Cache Keys

text8 lines
// CDN was using ONLY the URL path as cache key
// So these all got the same cached response:
// User A: GET /api/user/cart (Authorization: Bearer token_A)  
// User B: GET /api/user/cart (Authorization: Bearer token_B)  ← Same cache key!
// User C: GET /api/user/cart (Authorization: Bearer token_C)  ← Same cache key!

// Cache key should have been:
const cacheKey = `${url_path}:${authorization_header}:${user_id}`;
DiagramThe misconfiguration chain of disasters
%% title: The misconfiguration chain of disasters
flowchart TD
    R@{ icon: "sd:cdn", label: "CDN rule · pattern /api/*<br/>cache_ttl: 300 · cache_key: url_path" } --> IG["respect_origin_headers: false"]:::bad
    BE["Backend sends<br/>Cache-Control: private, no-cache<br/>Vary: Authorization"]:::core -->|"ignored"| IG
    IG --> K["URL-only cache key"]:::cache
    U@{ icon: "sd:users", label: "User A · Bearer token_A<br/>User B · Bearer token_B<br/>User C · Bearer token_C" } -->|"GET /api/user/cart"| K
    K --> SAME(["Same cached response<br/>for every user"]):::bad
    class R edge
    class U client
ExplainerCache poisoning: a cache key that ignores the user

ShopFast's CDN cached /api/* for 300 s with cache_key url_path and ignored the origin's headers. User A's GET /api/user/cart missed and was stored under the path alone, so User B, with a different Bearer token, got a HIT on User A's cart (x-cache-hits: 47). The fix makes /api/user/* bypass the cache (ttl 0, private) so the origin answers each token, while public /api/products/* pages stay cached by path.

The Perfect Storm Conditions#

Why It Happened on Black Friday:

  1. High traffic caused more cache hits on personal endpoints
  2. New CDN configuration deployed week before (increased aggressive caching)
  3. Load testing didn't include multi-user scenarios with same URL paths
  4. Monitoring only checked backend performance, not edge behavior

The Geographic Pattern:

text11 lines
// Some edge locations were more affected because:
// 1. Different cache configurations per region
// 2. Some edges had "respect_origin_headers: true" (working correctly)
// 3. Others had "respect_origin_headers: false" (broken)

const edgeConfigs = {
  'us-east-1': { respect_origin_headers: true },   // Working
  'us-west-1': { respect_origin_headers: false },  // Broken  
  'eu-west-1': { respect_origin_headers: false },  // Broken
  'ap-southeast-1': { respect_origin_headers: true } // Working
};

The Robust Caching Strategy: Never Again#

Endpoint Classification System#

text74 lines
// Comprehensive endpoint classification
const endpointTypes = {
  STATIC: {
    patterns: ['/assets/*', '/images/*', '/css/*', '/js/*'],
    cache_ttl: 86400,  // 24 hours
    cache_key: 'url_path',
    public: true
  },
  
  SEMI_STATIC: {
    patterns: ['/api/products/*', '/api/categories/*', '/api/search*'],
    cache_ttl: 300,    // 5 minutes  
    cache_key: 'url_path + query_params',
    public: true,
    vary_headers: ['Accept-Language', 'X-Currency']
  },
  
  USER_SPECIFIC: {
    patterns: ['/api/user/*', '/api/cart/*', '/api/orders/*'],
    cache_ttl: 0,      // NO CACHING
    bypass_cache: true,
    private: true
  },
  
  SESSION_DEPENDENT: {
    patterns: ['/api/recommendations/*', '/api/wishlist/*'],
    cache_ttl: 60,     // 1 minute max
    cache_key: 'url_path + user_id + session_id',
    private: true,
    vary_headers: ['Authorization', 'Cookie']
  }
};

// Middleware to enforce caching rules
function applyCachingStrategy(req, res, next) {
  const endpoint = classifyEndpoint(req.path);
  
  switch(endpoint.type) {
    case 'STATIC':
      res.set({
        'Cache-Control': `public, max-age=${endpoint.cache_ttl}`,
        'X-Cache-Type': 'static'
      });
      break;
      
    case 'SEMI_STATIC':
      res.set({
        'Cache-Control': `public, max-age=${endpoint.cache_ttl}`,
        'Vary': endpoint.vary_headers.join(', '),
        'X-Cache-Type': 'semi-static'
      });
      break;
      
    case 'USER_SPECIFIC':
      res.set({
        'Cache-Control': 'private, no-cache, no-store, must-revalidate',
        'Pragma': 'no-cache',
        'Expires': '0',
        'X-Cache-Type': 'user-specific'
      });
      break;
      
    case 'SESSION_DEPENDENT':
      res.set({
        'Cache-Control': `private, max-age=${endpoint.cache_ttl}`,
        'Vary': 'Authorization, Cookie',
        'X-User-Context': req.user?.id || 'anonymous',
        'X-Cache-Type': 'session-dependent'
      });
      break;
  }
  
  next();
}
DiagramEndpoint classification
%% title: Endpoint classification
flowchart LR
    REQ(["Request path"]):::client --> C{"classifyEndpoint"}:::core
    C --> ST["STATIC · /assets/* /images/* /css/* /js/*<br/>public · max-age 86400 (24 hours)<br/>key: url_path"]:::cache
    C --> SS["SEMI_STATIC · /api/products/* /api/categories/* /api/search*<br/>public · max-age 300 (5 minutes)<br/>key: url_path + query_params<br/>Vary: Accept-Language, X-Currency"]:::cache
    C --> US["USER_SPECIFIC · /api/user/* /api/cart/* /api/orders/*<br/>NO CACHING · ttl 0 · bypass<br/>private, no-cache, no-store"]:::bad
    C --> SD["SESSION_DEPENDENT · /api/recommendations/* /api/wishlist/*<br/>private · 1 minute max<br/>key: url_path + user_id + session_id"]:::queue

Defense-in-Depth Caching Architecture#

text109 lines
class SecureCacheManager {
  constructor() {
    this.sensitivePatterns = [
      /\/api\/user\/.*$/,
      /\/api\/cart\/.*$/,
      /\/api\/orders\/.*$/,
      /\/api\/profile\/.*$/,
      /\/api\/payment-methods\/.*$/,
      /\/api\/addresses\/.*$/
    ];
  }
  
  // Layer 1: Application-level protection
  protectSensitiveEndpoint(req, res, next) {
    const isSensitive = this.sensitivePatterns.some(pattern => 
      pattern.test(req.path)
    );
    
    if (isSensitive) {
      // Multiple layers of protection
      res.set({
        'Cache-Control': 'private, no-cache, no-store, must-revalidate',
        'Pragma': 'no-cache',
        'Expires': '0',
        'Surrogate-Control': 'no-store',  // Varnish
        'X-Accel-Expires': '0',           // Nginx
        'CDN-Cache-Control': 'no-store',  // CDN-targeted (Cloudflare and others)
        'X-Security-Policy': 'no-cache-sensitive'
      });
      
      // Add user context to prevent cross-contamination
      if (req.user) {
        res.set('X-User-ID', req.user.id);
        res.set('Vary', 'Authorization, Cookie');  // Vary names *request* headers
      }
    }
    
    next();
  }
  
  // Layer 2: CDN-level validation
  validateCacheHeaders(req, res, next) {
    res.on('finish', () => {
      const isSensitive = this.sensitivePatterns.some(pattern => 
        pattern.test(req.path)
      );
      
      if (isSensitive) {
        const cacheControl = res.get('Cache-Control');
        
        // Alert if sensitive endpoint doesn't have proper headers
        if (!cacheControl?.includes('private') || 
            !cacheControl?.includes('no-cache')) {
          
          console.error('SECURITY ALERT: Sensitive endpoint without proper cache headers', {
            path: req.path,
            cacheControl,
            userAgent: req.get('User-Agent'),
            timestamp: new Date().toISOString()
          });
          
          // Send alert to security team
          this.sendSecurityAlert('cache_header_violation', {
            endpoint: req.path,
            headers: res.getHeaders()
          });
        }
      }
    });
    
    next();
  }
  
  // Layer 3: Runtime cache verification
  async verifyCacheIntegrity() {
    const testEndpoints = [
      '/api/user/profile',
      '/api/cart/items',
      '/api/orders/recent'
    ];
    
    for (const endpoint of testEndpoints) {
      // Test with different user tokens
      const responses = await Promise.all([
        this.testRequest(endpoint, 'user_token_1'),
        this.testRequest(endpoint, 'user_token_2'),
        this.testRequest(endpoint, 'user_token_3')
      ]);
      
      // Verify responses are different (not cached cross-user)
      const uniqueResponses = new Set(responses.map(r => JSON.stringify(r.data)));
      
      if (uniqueResponses.size !== responses.length) {
        console.error('CACHE POISONING DETECTED', {
          endpoint,
          uniqueResponses: uniqueResponses.size,
          totalRequests: responses.length,
          responses: responses.map(r => ({ 
            user: r.userId, 
            cacheStatus: r.headers['cf-cache-status'] 
          }))
        });
        
        // Emergency cache purge
        await this.emergencyCachePurge(endpoint);
      }
    }
  }
}
DiagramThree layers of protection
%% title: Three layers of protection
flowchart TD
    REQ(["Request to a sensitive pattern<br/>/api/user · cart · orders · profile<br/>payment-methods · addresses"]):::client --> L1["Layer 1 · Application-level protection<br/>private, no-cache, no-store · Vary: Authorization, Cookie"]:::core
    L1 --> L2["Layer 2 · CDN-level validation"]:::edge
    L2 --> V{"private and no-cache?"}
    V -->|"no"| AL["SECURITY ALERT<br/>sendSecurityAlert cache_header_violation"]:::bad
    V ~~~ L3
    L3["Layer 3 · Runtime cache verification<br/>same endpoint · user_token_1, 2, 3"]:::service --> U{"All responses different?"}
    U -->|"no"| P(["CACHE POISONING DETECTED<br/>emergency cache purge"]):::bad
    U -->|"yes"| OK(["Not cached cross-user"]):::ok

Smart Cache Key Generation#

text41 lines
class SmartCacheKeyGenerator {
  generateCacheKey(req) {
    const baseKey = `${req.method}:${req.path}`;
    
    // Check if endpoint requires user context
    if (this.isUserSpecific(req.path)) {
      const userId = req.user?.id || 'anonymous';
      const sessionId = req.sessionID;
      return `${baseKey}:user:${userId}:session:${sessionId}`;
    }
    
    // Check if endpoint varies by location/currency
    if (this.isLocationSensitive(req.path)) {
      const country = req.headers['cf-ipcountry'] || 'unknown';
      const currency = req.headers['x-currency'] || 'USD';
      return `${baseKey}:geo:${country}:currency:${currency}`;
    }
    
    // Check if endpoint varies by language
    if (this.isLanguageSensitive(req.path)) {
      const language = req.headers['accept-language']?.split(',')[0] || 'en';
      return `${baseKey}:lang:${language}`;
    }
    
    // Default: just URL and query parameters
    const queryString = new URLSearchParams(req.query).toString();
    return queryString ? `${baseKey}:${queryString}` : baseKey;
  }
  
  isUserSpecific(path) {
    return /\/(user|cart|orders|profile|wishlist|addresses)\//.test(path);
  }
  
  isLocationSensitive(path) {
    return /\/(shipping|pricing|taxes|availability)\//.test(path);
  }
  
  isLanguageSensitive(path) {
    return /\/(products|search|categories)\//.test(path);
  }
}

Preventive Measures: The Never Again Checklist#

Automated Testing for Cache Scenarios#

text61 lines
// Multi-user cache poisoning test
describe('Cache Security Tests', () => {
  test('Personal endpoints should never cache across users', async () => {
    const personalEndpoints = [
      '/api/user/profile',
      '/api/cart/items', 
      '/api/orders/history'
    ];
    
    for (const endpoint of personalEndpoints) {
      // Create requests with different user tokens
      const user1Response = await request(app)
        .get(endpoint)
        .set('Authorization', 'Bearer user1_token')
        .expect(200);
        
      const user2Response = await request(app)
        .get(endpoint)
        .set('Authorization', 'Bearer user2_token')
        .expect(200);
      
      // Responses should be different (not cached)
      expect(user1Response.body).not.toEqual(user2Response.body);
      
      // Should have proper cache headers
      expect(user1Response.headers['cache-control']).toMatch(/private|no-cache/);
      expect(user2Response.headers['cache-control']).toMatch(/private|no-cache/);
      
      // Should not have cache hit headers
      expect(user1Response.headers['cf-cache-status']).not.toBe('HIT');
      expect(user2Response.headers['cf-cache-status']).not.toBe('HIT');
    }
  });
  
  test('CDN should respect origin cache headers', async () => {
    // Test that CDN doesn't override application cache settings
    const response = await request(cdnUrl)
      .get('/api/user/profile')
      .set('Authorization', 'Bearer test_token');
    
    expect(response.headers['cache-control']).toMatch(/private/);
    expect(response.headers['cf-cache-status']).toBe('BYPASS');
  });
  
  test('Static content should cache properly', async () => {
    const staticEndpoints = [
      '/assets/app.js',
      '/images/logo.png',
      '/api/products/123'
    ];
    
    for (const endpoint of staticEndpoints) {
      const response1 = await request(cdnUrl).get(endpoint);
      const response2 = await request(cdnUrl).get(endpoint);
      
      // Second request should be cached
      expect(response2.headers['cf-cache-status']).toBe('HIT');
      expect(response1.body).toEqual(response2.body);
    }
  });
});

Real-Time Monitoring and Alerting#

text70 lines
class CacheSecurityMonitor {
  constructor() {
    this.alerts = new AlertManager();
    this.metrics = new MetricsCollector();
  }
  
  // Monitor for cache poisoning indicators
  async monitorCacheBehavior() {
    setInterval(async () => {
      await this.checkCrossPollination();
      await this.validateCacheHeaders();
      await this.monitorCacheHitRates();
    }, 30000); // Every 30 seconds
  }
  
  async checkCrossPollination() {
    // Test same endpoint with different user contexts
    const testResult = await this.multiUserCacheTest('/api/user/cart');
    
    if (testResult.possiblePoisoning) {
      await this.alerts.sendCritical('CACHE_POISONING_DETECTED', {
        endpoint: '/api/user/cart',
        evidence: testResult.evidence,
        affectedUsers: testResult.affectedUsers,
        action: 'IMMEDIATE_CACHE_PURGE_REQUIRED'
      });
    }
  }
  
  async validateCacheHeaders() {
    const sensitiveEndpoints = [
      '/api/user/profile',
      '/api/cart/items',
      '/api/orders/recent'
    ];
    
    for (const endpoint of sensitiveEndpoints) {
      const response = await fetch(`${CDN_URL}${endpoint}`, {
        headers: { 'Authorization': 'Bearer test_token' }
      });
      
      const cacheControl = response.headers.get('cache-control');
      const cfCacheStatus = response.headers.get('cf-cache-status');
      
      // Alert if sensitive endpoint is being cached
      if (!cacheControl?.includes('private') || cfCacheStatus === 'HIT') {
        await this.alerts.sendWarning('SENSITIVE_ENDPOINT_CACHED', {
          endpoint,
          cacheControl,
          cfCacheStatus,
          recommendation: 'Review CDN configuration'
        });
      }
    }
  }
  
  async monitorCacheHitRates() {
    const cacheMetrics = await this.metrics.getCacheMetrics();
    
    // Alert if user-specific endpoints have high cache hit rates
    const userEndpointHitRate = cacheMetrics.hitRates['/api/user/*'];
    if (userEndpointHitRate > 0.1) { // >10% is suspicious
      await this.alerts.sendWarning('SUSPICIOUS_USER_ENDPOINT_CACHE_HITS', {
        hitRate: userEndpointHitRate,
        expected: '0%',
        action: 'INVESTIGATE_CDN_CONFIGURATION'
      });
    }
  }
}

Configuration Validation and Contracts#

text68 lines
# CDN Configuration Schema with Validation
cdn_config_schema:
  cache_rules:
    type: array
    items:
      type: object
      properties:
        pattern:
          type: string
          description: "URL pattern to match"
        cache_behavior:
          type: string
          enum: ["cache", "bypass", "conditional"]
        cache_ttl:
          type: integer
          minimum: 0
          maximum: 86400
        cache_key_includes:
          type: array
          items:
            enum: ["url_path", "query_params", "user_id", "session_id", "auth_header"]
        security_classification:
          type: string
          enum: ["public", "semi-public", "private", "sensitive"]
          description: "Data sensitivity level"
      required: ["pattern", "cache_behavior", "security_classification"]
      
  # Security constraints
  constraints:
    - if:
        properties:
          security_classification:
            const: "private"
      then:
        properties:
          cache_behavior:
            const: "bypass"
        required: ["cache_behavior"]
        
    - if:
        properties:
          security_classification:
            const: "sensitive"  
      then:
        properties:
          cache_behavior:
            const: "bypass"
          cache_ttl:
            const: 0
        required: ["cache_behavior", "cache_ttl"]

# Example valid configuration
cache_rules:
  - pattern: "/api/products/*"
    cache_behavior: "cache"
    cache_ttl: 300
    cache_key_includes: ["url_path", "query_params"]
    security_classification: "public"
    
  - pattern: "/api/user/*"
    cache_behavior: "bypass"
    cache_ttl: 0
    security_classification: "private"
    
  - pattern: "/api/cart/*"
    cache_behavior: "bypass" 
    cache_ttl: 0
    security_classification: "sensitive"

Pre-Deployment Cache Security Checklist#

text81 lines
// Automated pre-deployment checks
class CacheSecurityValidator {
  async validateDeployment(config) {
    const results = [];
    
    // Check 1: Ensure sensitive endpoints bypass cache
    results.push(await this.validateSensitiveEndpoints(config));
    
    // Check 2: Verify cache key generation includes user context
    results.push(await this.validateCacheKeys(config));
    
    // Check 3: Test multi-user scenarios
    results.push(await this.testMultiUserScenarios(config));
    
    // Check 4: Validate cache headers for all endpoint types
    results.push(await this.validateCacheHeaders(config));
    
    const failures = results.filter(r => !r.passed);
    
    if (failures.length > 0) {
      throw new Error(`Cache security validation failed: ${JSON.stringify(failures)}`);
    }
    
    return { passed: true, checks: results.length };
  }
  
  async validateSensitiveEndpoints(config) {
    const sensitivePatterns = [
      '/api/user/*',
      '/api/cart/*', 
      '/api/orders/*',
      '/api/profile/*',
      '/api/payment-methods/*'
    ];
    
    const violations = [];
    
    for (const pattern of sensitivePatterns) {
      const rule = config.cache_rules.find(r => 
        new RegExp(r.pattern.replace('*', '.*')).test(pattern)
      );
      
      if (!rule || rule.cache_behavior !== 'bypass') {
        violations.push({
          pattern,
          issue: 'Sensitive endpoint not configured to bypass cache',
          currentConfig: rule
        });
      }
    }
    
    return {
      passed: violations.length === 0,
      violations
    };
  }
  
  async testMultiUserScenarios(config) {
    // Simulate deployment and test with multiple users
    const testEndpoints = ['/api/user/profile', '/api/cart/items'];
    const violations = [];
    
    for (const endpoint of testEndpoints) {
      const user1Data = await this.simulateRequest(endpoint, 'user1_token');
      const user2Data = await this.simulateRequest(endpoint, 'user2_token');
      
      if (JSON.stringify(user1Data) === JSON.stringify(user2Data)) {
        violations.push({
          endpoint,
          issue: 'Same response returned for different users',
          evidence: { user1Data, user2Data }
        });
      }
    }
    
    return {
      passed: violations.length === 0,
      violations
    };
  }
}

The Business Impact and Recovery#

Immediate Damage Assessment#

Customer Impact:

  • 15,000 affected users across 12 edge locations
  • 347 customer support tickets filed within 2 hours
  • 23 customers saw partial payment information
  • 1,200+ users experienced wrong shopping cart contents
  • Average resolution time: 4.2 hours per affected customer

Financial Impact:

text26 lines
const damageAssessment = {
  directCosts: {
    lostSales: 2400000,        // $2.4M in abandoned carts
    supportOvertime: 75000,     // Extra support staff
    engineeringTime: 150000,    // Emergency response team
    cdnOverage: 25000          // Cache purge and traffic costs
  },
  
  potentialCosts: {
    gdprFines: 50000000,       // GDPR cap: 4% of global turnover (or €20M if higher)
    lawsuits: 5000000,         // Estimated class action exposure
    brandDamage: 10000000,     // Estimated trust/reputation cost
    customerChurn: 8000000     // Lost lifetime value
  },
  
  mitigationCosts: {
    securityAudit: 200000,     // Third-party security review
    toolingUpgrade: 500000,    // Better monitoring/testing tools
    staffingIncrease: 1000000, // Additional security engineers
    insurancePremium: 300000   // Increased cyber insurance
  }
};

console.log('Total immediate impact:', 
  Object.values(damageAssessment.directCosts).reduce((a, b) => a + b, 0)
); // $2.65M

The Recovery Strategy#

Phase 1: Immediate Containment (0-30 minutes)

  1. Emergency cache purge across all CDN edges
  2. Force bypass for all personal endpoints
  3. Customer communication via email/SMS
  4. Support team briefing and script updates

Phase 2: Investigation and Fix (30 minutes - 4 hours)

  1. Root cause analysis and documentation
  2. Permanent CDN configuration fixes
  3. Enhanced monitoring deployment
  4. Security team incident response

Phase 3: Customer Recovery (4 hours - 2 weeks)

  1. Individual customer outreach and explanation
  2. Account security reviews and password resets
  3. Compensation offers (discounts, free shipping)
  4. GDPR compliance notifications where required

Lessons Learned: The Philosophical Shift#

The Hidden Complexity of "Simple" Caching#

This incident revealed that caching is not just a performance optimization—it's a security boundary. Every cached response is a potential data leak waiting to happen.

The False Security of Backend-Only Thinking:

text10 lines
// Developers thought this was enough:
app.get('/api/user/cart', authenticate, (req, res) => {
  res.set('Cache-Control', 'private, no-cache');
  res.json(getUserCart(req.user.id));
});

// But CDN configuration can override ANY backend header:
cdn_config:
  override_origin_headers: true  # ← This kills security
  default_cache_ttl: 300        # ← This caches everything

The New Security Mindset:

  • Assume CDN misconfiguration: Always test edge behavior, not just origin
  • Defense in depth: Application headers + CDN rules + monitoring + testing
  • User context in cache keys: Never cache based on URL alone for personal data
  • Continuous validation: Automated testing for cross-user contamination

The Economics of Edge Security#

Traditional Cost Thinking:

  • CDN saves bandwidth: ✅ Good
  • CDN improves performance: ✅ Good
  • CDN reduces server load: ✅ Good

Security-Aware Cost Thinking:

  • CDN saves bandwidth: ✅ Good, but verify cache keys include user context
  • CDN improves performance: ✅ Good, but never for personal data
  • CDN reduces server load: ✅ Good, but monitor for cross-contamination
  • CDN misconfiguration liability: ❌ Potential $50M+ in fines and lawsuits

The Organizational Changes#

Before the Incident:

  • CDN managed by DevOps team
  • Security team focused on application layer
  • No cross-team cache configuration reviews
  • Testing only validated functional requirements

After the Incident:

  • CDN changes require security team approval
  • Mandatory multi-user testing for all endpoints
  • Real-time cache behavior monitoring
  • Cache configuration treated as code with peer review

Summary: When Performance Meets Privacy#

The ShopFast cache poisoning incident represents a perfect storm of modern web architecture complexity. What appeared to be a simple performance optimization—caching API responses at the edge—became a massive privacy violation that exposed thousands of customers' personal data.

The Technical Lessons:

  1. CDN configuration is security configuration: Never treat caching rules as purely performance optimizations. Every cache rule is a potential security boundary.

  2. User context must be in cache keys: Any endpoint that returns user-specific data must include user identifiers in the cache key, or bypass caching entirely.

  3. Backend headers can be ignored: Don't assume CDN will respect your application's cache-control headers. Verify edge behavior independently.

  4. Testing must include edge scenarios: Load testing that only validates backend performance misses critical edge layer bugs.

The Business Lessons:

  1. Performance optimizations can create security vulnerabilities: The same CDN that improves user experience can become a data leak vector.

  2. Edge computing requires edge security thinking: As more logic moves to CDN edge nodes, security must follow.

  3. Incident response must include customer communication: Technical fixes aren't enough—customer trust recovery is equally critical.

  4. Compliance violations can dwarf technical costs: The potential GDPR fines ($50M+) far exceeded the engineering costs to prevent the issue.

The Ultimate Truth:

In the modern web architecture, there is no such thing as "just a performance optimization." Every technical decision—from cache TTLs to CDN rules to database indexes—has security implications. The line between performance engineering and security engineering has blurred to the point of being meaningless.

David's team learned that building secure, high-performance systems requires thinking about the entire request journey—from user's browser through CDN edges to origin servers and back. A single misconfigured cache rule at any point in that journey can turn a performance feature into a privacy nightmare.

The Final Lesson: In distributed systems, performance and security are not separate concerns—they are inseparable aspects of the same system design challenge. Optimize for performance by all means, but never forget that every cached response is a potential security incident waiting to happen.

The next time you add max-age=300 to a cache header, ask yourself: "Whose data am I caching, and who might see it?" The answer might save your company millions of dollars and thousands of customers' trust.

End of file · CASE-004

Read it through to close the case.

0/9 sections read. Each one counts after a few minutes of actual reading in it; skipping ahead doesn't. The case closes itself once every section is read.

All case files →
> debrief --interactiveCASE-004 · +25 XP at 6/8
End-of-file activity · spot the bug

Find the leak

10:47 AM, Black Friday: 47 people just got the same shopping cart. Find the bad lines in ShopFast's CDN config, fix the cache key in a live replay, and classify four endpoints.

  • ~3 MIN
  • 3 STEPS · 8 PTS
  • SOLVE AT 6/8
  • +25 XP

this case in shorts · 2

play them all →

One idea from this case in about a minute each, made for your phone.

CDNCache PoisoningSecurityData BreachE-commerceProduction IncidentsBlack Friday

Discussion

Add to the post-mortem
loading the record…