Tenant-Aware Caching Bugs

Cache PoisoningMulti-Tenant CachingCross-Tenant Cache Leakage

Tenant-Aware Caching Bugs at a glance

What it is: Caching implementations that fail to properly isolate cached data by tenant, causing sensitive data from one tenant to be served to another tenant in multi-tenant SaaS applications.
Why it happens: One of the most subtle multi-tenant security issues
How to fix: Always include tenant_id in cache keys; Validate tenant context before serving cached responses; Use tenant-aware cache namespaces or prefixes; Test caching with multiple tenant contexts

Overview

Tenant-aware caching bugs occur in multi-tenant SaaS applications when cache keys don't include tenant identifiers, causing data from one tenant to be cached and then served to different tenants. This is particularly insidious because it appears intermittent - only occurring when the cache is warm from another tenant's request.

Common scenarios include HTTP response caching without tenant context, application-level caching (Redis, Memcached) with tenant-agnostic keys, CDN caching without tenant-specific headers, GraphQL query caching without tenant scope, and session storage shared across tenants. The bug often manifests as users occasionally seeing other tenants' data - seemingly at random.

sequenceDiagram participant TenantA as Tenant A User participant App participant Cache participant DB participant TenantB as Tenant B User TenantA->>App: GET /api/customers App->>Cache: GET cache:customers (no tenant in key!) Cache-->>App: Cache miss App->>DB: SELECT * FROM customers WHERE tenant_id='A' DB-->>App: Tenant A customers App->>Cache: SET cache:customers = Tenant A data App-->>TenantA: Tenant A customers TenantB->>App: GET /api/customers App->>Cache: GET cache:customers (same key!) Cache-->>App: Cache hit (Tenant A data) App-->>TenantB: Tenant A customers (leaked!) Note over Cache: Missing: Tenant ID in cache key
A potential flow for a Tenant-Aware Caching Bugs exploit

Where it occurs

Caching bugs occur from cache keys not including tenant_id, HTTP caching headers without Vary or tenant-specific directives, shared cache instances without tenant namespaces, CDN caching of multi-tenant responses, GraphQL/API response caching without tenant context, and missing validation of tenant context when serving cached data.

Impact

Tenant caching bugs cause cross-tenant data exposure where users see other organizations' sensitive data, compliance violations under data protection regulations, loss of customer trust and contract terminations, legal liability from data breaches, and intermittent bugs that are difficult to reproduce and fix.

Prevention

Always include tenant_id in all cache keys at every caching layer. Use cache key prefixes or namespaces that include tenant context. For HTTP caching, use Vary header with tenant-identifying cookies/headers. Disable HTTP caching for multi-tenant endpoints or use tenant-specific cache directives. For CDN caching, include tenant ID in cache key or disable CDN for tenant-specific content. Validate tenant context matches cached data before serving. Use separate cache instances per tenant for high-security applications. Implement comprehensive testing with multiple concurrent tenant sessions. Add monitoring to detect cross-tenant cache hits. Document all caching layers and tenant isolation requirements.

Examples

Switch tabs to view language/framework variants.

Cache keys don't include tenant ID causing data leakage

Shared cache keys expose tenant data across tenants.

Vulnerable
Python • Flask + Redis — Bad
from flask import Flask, request
import redis

app = Flask(__name__)
cache = redis.Redis()

@app.route('/dashboard')
def dashboard():
    # BUG: Cache key doesn't include tenant_id
    cache_key = 'dashboard_data'
    
    cached = cache.get(cache_key)
    if cached:
        return cached
    
    data = get_dashboard_data(request.tenant_id)
    cache.set(cache_key, data, ex=300)
    return data
  • Line 10: Cache key without tenant isolation

Shared cache keys cause tenant data to be served to wrong tenants.

Secure
Python • Flask + Redis — Good
from flask import Flask, request
import redis

app = Flask(__name__)
cache = redis.Redis()

@app.route('/dashboard')
def dashboard():
    tenant_id = request.tenant_id
    
    # Include tenant_id in cache key
    cache_key = f'dashboard_data:tenant:{tenant_id}'
    
    cached = cache.get(cache_key)
    if cached:
        return cached
    
    data = get_dashboard_data(tenant_id)
    cache.set(cache_key, data, ex=300)
    return data
  • Line 12: Tenant ID included in key

Always include tenant ID in cache keys for multi-tenant applications.

Engineer Checklist

  • Include tenant_id in all cache keys

  • Use cache key prefixes/namespaces with tenant context

  • Add Vary header for HTTP caching with tenant-identifying headers

  • Disable CDN caching for multi-tenant endpoints

  • Validate tenant context matches before serving cached data

  • Use tenant-specific cache namespaces (Redis databases)

  • Test caching with multiple concurrent tenant sessions

  • Monitor for cross-tenant cache hits

  • Document caching strategy for all layers

  • Audit all caching code for tenant isolation

  • Use separate cache instances for tenants if possible

  • Clear tenant caches on tenant data changes

End-to-End Example

An application caches API responses without including tenant_id in the cache key, causing cross-tenant data leakage.

Vulnerable
PYTHON
# Vulnerable: No tenant in cache key
import redis

redis_client = redis.Redis()

@app.route('/api/customers')
@login_required
def get_customers():
    # Vulnerable: Cache key has no tenant context!
    cache_key = 'customers:list'
    
    cached = redis_client.get(cache_key)
    if cached:
        return jsonify(json.loads(cached))
    
    # Fetch for current user's tenant
    customers = Customer.query.filter_by(
        tenant_id=current_user.tenant_id
    ).all()
    
    data = [c.to_dict() for c in customers]
    
    # Cache without tenant context
    redis_client.setex(cache_key, 300, json.dumps(data))
    
    return jsonify(data)
Secure
PYTHON
# Secure: Tenant-aware cache keys
import redis
import hashlib

redis_client = redis.Redis()

@app.route('/api/customers')
@login_required
def get_customers():
    tenant_id = current_user.tenant_id
    
    # Include tenant_id in cache key
    cache_key = f'tenant:{tenant_id}:customers:list'
    
    cached = redis_client.get(cache_key)
    if cached:
        cached_data = json.loads(cached)
        
        # Validate tenant context matches (defense in depth)
        if cached_data.get('tenant_id') == tenant_id:
            return jsonify(cached_data['customers'])
        else:
            # Cache poisoning detected!
            log_security_event('cache_tenant_mismatch', tenant_id, cache_key)
            redis_client.delete(cache_key)
    
    # Fetch for current user's tenant
    customers = Customer.query.filter_by(
        tenant_id=tenant_id
    ).all()
    
    data = [c.to_dict() for c in customers]
    
    # Cache with tenant validation data
    cache_data = {
        'tenant_id': tenant_id,
        'customers': data,
        'cached_at': datetime.utcnow().isoformat()
    }
    
    redis_client.setex(cache_key, 300, json.dumps(cache_data))
    
    return jsonify(data)

# Alternative: Use separate Redis databases per tenant
def get_tenant_cache(tenant_id):
    # Use tenant-specific Redis database
    db_num = hash(tenant_id) % 16  # Redis has 16 databases
    return redis.Redis(db=db_num)

Discovery

Use two different tenant accounts simultaneously and observe if data from one tenant appears in the other's responses.

  1. 1. Test cache key collisions

    http

    Action

    Access resource after another tenant

    Request

    GET https://app.example.com/dashboard

    Response

    Status: 200
    Body:
    {
      "note": "Other tenant's data served from cache"
    }

    Artifacts

    cache_collision cross_tenant_leak
  2. 2. Test cache headers for tenant isolation

    http

    Action

    Check if Vary header includes tenant ID

    Request

    GET https://app.example.com/data

    Response

    Status: 200
    Body:
    {
      "note": "Cache doesn't vary by tenant, data shared"
    }

    Artifacts

    missing_vary_header cache_sharing
  3. 3. Test CDN cache poisoning

    http

    Action

    Poison cache with tenant-specific data

    Request

    GET https://app.example.com/api?tenant=victim

    Response

    Status: 200
    Body:
    {
      "note": "Victim's data cached globally"
    }

    Artifacts

    cache_poisoning data_leak

Exploit steps

Attacker with two accounts (or timing attacks) can prime the cache with their data and observe it being served to other tenants, or vice versa.

  1. 1. Access other tenant's cached data

    Cross-tenant cache exploitation

    http

    Action

    Retrieve cached responses meant for other tenants

    Request

    GET https://app.example.com/api/sensitive-data

    Response

    Status: 200
    Body:
    {
      "note": "Other tenant's PII and business data accessible"
    }

    Artifacts

    tenant_data_leak pii_exposure cache_bypass
  2. 2. Poison shared cache with malicious content

    Cache poisoning attack

    http

    Action

    Inject malicious content into shared cache

    Request

    GET https://app.example.com/dashboard?xss=<script>...

    Response

    Status: 200
    Body:
    {
      "note": "Malicious content served to all tenants"
    }

    Artifacts

    xss_cache_poisoning mass_compromise
  3. 3. Exfiltrate tenant data via cache timing

    Cache timing side-channel

    http

    Action

    Use cache timing to determine tenant presence/data

    Request

    GET measure response times for /api/user/email@victim.com

    Response

    Status: 200
    Body:
    {
      "note": "Cache timing reveals if user exists in tenant"
    }

    Artifacts

    user_enumeration information_disclosure

Specific Impact

Cross-tenant data breach exposing sensitive customer information, compliance violations, and loss of customer trust in the SaaS platform's security.

Fix

Always include tenant_id in cache keys. Add tenant validation when serving cached data as defense in depth. Consider using separate cache instances or databases per tenant for critical applications. Log and alert on cache tenant mismatches.

Detect This Vulnerability in Your Code

Sourcery automatically identifies tenant-aware caching bugs vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free