Tenant-Aware Caching Bugs
Tenant-Aware Caching Bugs at a glance
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.
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.
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.
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: 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: 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. Test cache key collisions
httpAction
Access resource after another tenant
Request
GET https://app.example.com/dashboardResponse
Status: 200Body:{ "note": "Other tenant's data served from cache" }Artifacts
cache_collision cross_tenant_leak -
2. Test cache headers for tenant isolation
httpAction
Check if Vary header includes tenant ID
Request
GET https://app.example.com/dataResponse
Status: 200Body:{ "note": "Cache doesn't vary by tenant, data shared" }Artifacts
missing_vary_header cache_sharing -
3. Test CDN cache poisoning
httpAction
Poison cache with tenant-specific data
Request
GET https://app.example.com/api?tenant=victimResponse
Status: 200Body:{ "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. Access other tenant's cached data
Cross-tenant cache exploitation
httpAction
Retrieve cached responses meant for other tenants
Request
GET https://app.example.com/api/sensitive-dataResponse
Status: 200Body:{ "note": "Other tenant's PII and business data accessible" }Artifacts
tenant_data_leak pii_exposure cache_bypass -
2. Poison shared cache with malicious content
Cache poisoning attack
httpAction
Inject malicious content into shared cache
Request
GET https://app.example.com/dashboard?xss=<script>...Response
Status: 200Body:{ "note": "Malicious content served to all tenants" }Artifacts
xss_cache_poisoning mass_compromise -
3. Exfiltrate tenant data via cache timing
Cache timing side-channel
httpAction
Use cache timing to determine tenant presence/data
Request
GET measure response times for /api/user/email@victim.comResponse
Status: 200Body:{ "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