# Multi-Tenant Data Isolation Enforced

- **Pattern:** `ab-002272` (`saas-authorization.admin-privilege.multi-tenant-isolation`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002272
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002272)

## Why it matters

Multi-tenant applications that derive the active tenant ID from a client-supplied header, query parameter, or request body — rather than from the authenticated session — allow any authenticated user to access any organization's data by supplying that org's ID. CWE-284 (Improper Access Control) and CWE-639 (Authorization Bypass Through User-Controlled Key) both apply. OWASP A01 (Broken Access Control) lists tenant isolation failures as a primary risk. NIST 800-53 AC-3 and SC-4 (Information in Shared Resources) require that tenant boundaries be enforced by the system, not honored on trust from the client.

## Severity rationale

High because any authenticated user can access any tenant's data by supplying a valid org ID — no brute force needed if org IDs are discoverable or guessable.

## Remediation

Derive the active tenant from the authenticated session — never from a user-supplied request parameter. If users can belong to multiple orgs, store the active org selection server-side.

```typescript
// Safe — tenant from session
const orgId = session.user.activeOrgId;

// Also safe — user supplies org ID but membership is verified first
const requestedOrgId = params.orgId;
const membership = await db.orgMember.findFirst({
  where: { orgId: requestedOrgId, userId: session.user.id },
});
if (!membership) return NextResponse.json({ error: 'Not found' }, { status: 404 });

// Never do this without membership verification:
// const orgId = req.headers.get('x-org-id');
```

For org-switching flows, validate the switch on the server and update `session.user.activeOrgId` — do not let the client instruct the server which org to use by header.

## Detection

- **ID:** `multi-tenant-isolation`
- **Severity:** `high`
- **What to look for:** Count all relevant instances and enumerate each. In multi-tenant routes, trace how the `orgId`, `teamId`, or `workspaceId` is determined. Is it taken from the authenticated session (`session.user.orgId`, `session.activeOrganizationId`) or from a client-supplied value (`req.headers.get('x-org-id')`, `params.orgId`, `body.orgId`)? If the tenant context is derived from client input, is it validated against the user's membership before use?
- **Pass criteria:** Tenant context (the org/team/workspace ID used to scope queries) is always derived from the authenticated session, not from client-supplied request parameters. At least 1 implementation must be verified. Or, if a user can specify which org to act on behalf of, the route verifies the user is a member of that org before proceeding.
- **Fail criteria:** Tenant context is taken directly from a request parameter without validation — a user can supply any `orgId` and receive data from that organization's namespace.
- **Skip (N/A) when:** Multi-tenant concept not detected. No teams, organizations, or workspaces in the data model or routes.
- **Detail on fail:** `"Tenant context is derived from client-supplied [header/param/body field] without membership verification. Users can supply any org ID and access that organization's data."` (Note the specific parameter used.)
- **Remediation:** Always derive the active tenant from the authenticated session. If users can belong to multiple orgs, store the active org selection in the session (server-side), not in a cookie or request header the user can modify.

  ```typescript
  // Safe — tenant derived from session
  const orgId = session.user.activeOrgId; // Set server-side when user switches orgs

  // Also safe — user supplies org ID but membership is verified
  const requestedOrgId = params.orgId;
  const membership = await db.orgMember.findFirst({
    where: { orgId: requestedOrgId, userId: session.user.id },
  });
  if (!membership) return NextResponse.json({ error: 'Not found' }, { status: 404 });

  // Unsafe — trusting client without verification
  // const orgId = req.headers.get('x-org-id'); // NEVER DO THIS without membership check
  ```

## External references

- cwe CWE-284
- cwe CWE-639
- owasp A01
- nist AC-3
- nist SC-4

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-002272
