Cache keys include the tenant identifier
Why it matters
CWE-524 (Use of Cache Containing Sensitive Information) combined with OWASP A01 describes exactly this failure: a cache key like dashboard-stats that is populated by Tenant A's first request and then returned unchanged to Tenant B. Unlike database-layer isolation gaps, cache leaks are silent — no error is raised, the data looks valid, and the victim tenant never knows they received another org's numbers. The window for cross-tenant exposure is bounded only by the cache TTL, which is often measured in minutes or hours.
Severity rationale
High because a cache key collision silently serves one tenant's business data to another tenant with no error or signal that a leak has occurred.
Remediation
Prefix every cache key for tenant-specific data with the tenant identifier. Apply this to Redis, Upstash, Next.js unstable_cache, and React Query/SWR cache keys:
// Instead of:
const cacheKey = `dashboard-stats`
// Do this:
const cacheKey = `tenant:${session.user.organizationId}:dashboard-stats`
// For Next.js unstable_cache:
const getData = unstable_cache(
async () => fetchDashboardStats(orgId),
[`org-${orgId}-dashboard-stats`],
{ tags: [`org-${orgId}`, 'dashboard-stats'] }
)
// Invalidate with tenant scope:
await revalidateTag(`org-${orgId}`)
Audit cache invalidation paths too — a key-scoped write is safe, but a global flushAll in a multi-tenant cache is a denial-of-service risk for other tenants.
Detection
-
ID:
cache-keys-include-tenant -
Severity:
high -
What to look for: Examine all caching code — Redis/Upstash calls, in-memory caches, React Query/SWR cache keys, Next.js
unstable_cachecalls, ISR cache tags. Check whether cache keys include a tenant identifier to prevent one tenant's cached data from being served to another tenant's request. Look for patterns likecache.get('user-list')with no tenant in the key vs.cache.get(tenant:${tenantId}:user-list). -
Pass criteria: Count all cache set/get calls for tenant-specific data and confirm at least 100% include the tenant identifier as at least 1 component of the key. Cache invalidation also includes the tenant scope. Next.js cache tags include tenant identifiers when tagging tenant-specific data.
-
Fail criteria: Any cache key for tenant-specific data that does not include the tenant identifier. A request from Tenant A could receive cached data that was generated for Tenant B's request.
-
Skip (N/A) when: No caching layer is detected. Signal: no Redis, Upstash, ioredis, or cache-related dependencies; no
unstable_cacheusage; no in-memory cache patterns; no ISR revalidation patterns for dynamic tenant data. -
Detail on fail: Name the cache keys and what tenant data they contain. Example:
"Redis key 'dashboard-stats' used in src/lib/cache.ts does not include tenant identifier. This key is set from the first tenant's request and returned to subsequent requests from any tenant." -
Remediation: Always scope cache keys to the tenant:
// Instead of: const cacheKey = `dashboard-stats` // Do this: const cacheKey = `tenant:${session.user.organizationId}:dashboard-stats` // For Next.js unstable_cache: const getData = unstable_cache( async () => fetchDashboardStats(orgId), [`org-${orgId}-dashboard-stats`], { tags: [`org-${orgId}`, 'dashboard-stats'] } ) // For cache invalidation, always include tenant scope: await revalidateTag(`org-${orgId}`)
External references
- cwe · CWE-524 — Use of Cache Containing Sensitive Information
- cwe · CWE-285 — Improper Authorization
- owasp:2021 · A01 — Broken Access Control
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-multi-tenancy·automated