# Tenant context is set from authenticated session, not client input

- **Pattern:** `ab-002330` (`saas-multi-tenancy.data-isolation.tenant-context-from-session`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002330
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002330)

## Why it matters

Sourcing tenant context from client-supplied values — a request body `tenantId`, a query parameter, or an unverified path segment — is CWE-639 and OWASP A04 (Insecure Design) in combination: any user who can forge or swap that value gains access to an arbitrary tenant's data. This is not a theoretical risk; it is one of the most common multi-tenancy bugs because the attack requires only changing a single field in a normal API call. NIST AC-3 requires that access decisions be based on authoritative session state, not client assertions.

## Severity rationale

Critical because any authenticated user can swap a single request parameter to impersonate a different tenant and read or write their data.

## Remediation

Resolve tenant identity exclusively from the server-side session. If a tenant ID appears in the URL (e.g., `/api/orgs/:orgId/`), treat it as an untrusted assertion and cross-check it against the authenticated user's memberships before using it:

```ts
// src/middleware/tenantAuth.ts
const session = await getServerSession()
if (!session) return new Response('Unauthorized', { status: 401 })

const { orgId } = params
const membership = await db.memberships.findFirst({
  where: { userId: session.user.id, organizationId: orgId }
})
if (!membership) return new Response('Forbidden', { status: 403 })

// Safe to use orgId now — membership is confirmed
```

Never pass `tenantId` from request bodies into database `where` clauses without this validation step. Centralize the membership check in middleware so no route can accidentally skip it.

## Detection

- **ID:** `tenant-context-from-session`
- **Severity:** `critical`
- **What to look for:** Examine how tenant identity is established in each API route handler and middleware. Look for whether the tenant identifier (org ID, team ID, workspace ID) comes from: (a) the authenticated session/JWT (safe), or (b) a request body field, query parameter, or path parameter without session validation (unsafe). Check middleware for tenant resolution logic. Look for patterns like `const tenantId = req.body.tenantId` or `const tenantId = searchParams.get('tenantId')` used directly as a database filter without verification against the session.
- **Pass criteria:** List all route handlers that resolve a tenant context and confirm at least 100% source the tenant identifier exclusively from the authenticated session (JWT claims, session object, or a server-side lookup). Quote the actual session resolution code or middleware name of the authenticated user's tenant). On pass, report the ratio of session-sourced tenant resolutions to total resolutions. Path parameters like `/api/orgs/:orgId/` may appear in URLs, but the handler must verify that the authenticated user is a member of that org — the path parameter alone is not trusted authority.
- **Fail criteria:** Any route handler that reads a tenant identifier from the request body, query string, or headers and uses it directly in a database query must not pass, query string, or headers and uses it directly in a database query without cross-checking it against the authenticated user's allowed tenants. Any route that accepts `teamId` in a POST body and uses it as a filter without verifying the authenticated user belongs to that team.
- **Skip (N/A) when:** No authentication system is detected. Signal: no next-auth, clerk, lucia, supabase auth, firebase auth, auth0, or similar auth dependencies.
- **Detail on fail:** Name the routes and parameters involved. Example: `"POST /api/data/export reads tenantId from req.body.tenantId and uses it directly in database query without checking session membership in src/app/api/data/export/route.ts"`
- **Cross-reference:** Compare with `saas-multi-tenancy.data-isolation.every-query-tenant-scoped` — this check ensures tenant context comes from a trusted source; the query-scoping check ensures it is applied consistently.
- **Remediation:** Always resolve tenant context from the server-side session, never from client-provided values. Treat any tenant identifier in a URL or request body as an untrusted assertion to be validated:

  ```ts
  // Middleware or route handler
  const session = await getServerSession()
  if (!session) return new Response('Unauthorized', { status: 401 })

  // If tenantId appears in URL params, validate membership
  const { orgId } = params
  const membership = await db.memberships.findFirst({
    where: { userId: session.user.id, organizationId: orgId }
  })
  if (!membership) return new Response('Forbidden', { status: 403 })

  // Now use orgId from the validated path param (or use session.user.organizationId directly)
  ```

---

## External references

- cwe CWE-639
- cwe CWE-807
- owasp A01
- owasp A04

Taxons: access-control, injection-and-input-trust

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