When a user switches organizations, any prior-tenant data held in client-side state — React Query caches, SWR caches, Zustand stores, or sessionStorage — remains accessible until the browser is refreshed or the cache entry expires. CWE-524 and OWASP A01 both apply: the prior tenant's dashboards, user lists, and documents can be queried by API calls that still carry the old cache entries, or rendered from stale in-memory state. This is particularly dangerous in shared-device scenarios (kiosk, shared workstation) where the next person uses the same browser session.
High because incomplete session teardown lets prior-tenant data remain accessible through stale client-side cache or in-memory state after a switch.
On tenant switch, invalidate all client-side query caches and force a full page transition to a tenant-neutral route:
async function switchOrganization(newOrgId: string) {
// Update session server-side first
await updateSessionOrg(newOrgId)
// Clear all React Query cache entries
await queryClient.invalidateQueries()
// or for guaranteed clearance: queryClient.clear()
// Navigate to a safe landing page with a full reload
window.location.href = '/dashboard'
}
Prefer window.location.href (full page reload) over router.push() — client-side navigation leaves in-memory stores intact. If using Zustand or similar, call your store's reset action before the navigation.
ID: saas-multi-tenancy.tenant-boundaries.tenant-switching-no-leak
Severity: high
What to look for: If the application supports switching between organizations or workspaces (team switcher, org dropdown), examine the tenant-switching flow. Look for: session update logic, token refresh, client-side state reset, React Query/SWR cache clearing, and any in-memory state that is scoped to a tenant. Check whether switching tenants fully replaces the session tenant context or merely appends to it.
Pass criteria: Enumerate all tenant-switching paths. Switching to a different tenant: (a) updates the session to reflect the new tenant within no more than 500 milliseconds only, (b) clears or invalidates all client-side cached data from the previous tenant, and (c) redirects to a tenant-neutral or new-tenant starting page rather than remaining on a page that may have prior-tenant data rendered.
Fail criteria: After switching tenants, any prior tenant's data remains visible, accessible, or fetchable from the client state. The session still contains references to the previous tenant. API calls made immediately after switching return previous-tenant data because the session hasn't fully updated.
Skip (N/A) when: No tenant switching UI or mechanism is detected. Signal: no team switcher component, no "switch organization" flow, no multi-org session handling in auth or middleware.
Detail on fail: Describe what persists. Example: "React Query cache is not invalidated on tenant switch in src/components/OrgSwitcher.tsx. After switching orgs, cached API responses from the previous org remain in memory and are returned for identical query keys."
Remediation: When switching tenants, perform a full cache bust and session reset:
async function switchOrganization(newOrgId: string) {
// Update session server-side
await updateSessionOrg(newOrgId)
// Clear all client-side query cache
await queryClient.invalidateQueries() // React Query
// or: queryClient.clear()
// Navigate to a safe landing page for the new org
router.push(`/dashboard`)
}
Prefer a full page reload (window.location.href = '/dashboard') over client-side navigation when switching tenants — it guarantees all in-memory state is cleared.