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.
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.
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.
ID: saas-multi-tenancy.data-isolation.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_cache calls, 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 like cache.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_cache usage; 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}`)