# No cross-tenant data visible in API responses

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

## Why it matters

CWE-285 (Improper Authorization) and OWASP A01 describe exactly this failure: an API that returns records without verifying the requester owns them. For list endpoints, a missing tenant filter exposes every record in the table to every authenticated user. For single-resource endpoints, guessing or enumerating a UUID is enough to retrieve another tenant's document, project, or user record — no special privilege required. This constitutes unauthorized disclosure of confidential business data and, where PII is involved, a reportable data breach under GDPR Art. 33.

## Severity rationale

Critical because unauthenticated enumeration of resource IDs — or simply calling a list endpoint — exposes every tenant's data to any valid session token.

## Remediation

For single-resource lookups, always fetch by both `id` and `organizationId` together so the database enforces ownership at query time rather than after the fact:

```ts
// Instead of:
const doc = await db.documents.findUnique({ where: { id: params.id } })

// Do this:
const doc = await db.documents.findUnique({
  where: { id: params.id, organizationId: session.user.organizationId }
})
if (!doc) return new Response('Not found', { status: 404 })
```

For list endpoints, verify the underlying database query already has tenant scope applied (see `every-query-tenant-scoped`) — never filter in application memory after a broad fetch. Return 404 (not 403) on ownership mismatch to avoid confirming that the resource exists.

## Detection

- **ID:** `no-cross-tenant-api-responses`
- **Severity:** `critical`
- **What to look for:** Examine all API route handlers that return lists of resources (`GET /api/documents`, `GET /api/users`, `GET /api/projects`, etc.). Verify that every list endpoint either: (a) queries with a tenant scope already applied at the database level, or (b) explicitly filters the result set by the authenticated user's tenant before returning. Also check single-resource endpoints (`GET /api/documents/:id`) to ensure they verify the resource belongs to the requesting tenant before returning it.
- **Pass criteria:** Enumerate all API list endpoints and confirm at least 100% retrieve only records belonging to the current tenant. Every single-resource endpoint verifies tenant ownership before returning the resource (not just checking that the resource exists, but that it belongs to the requester's tenant). A request for a resource owned by a different tenant must not pass unless it returns 403 or 404 within no more than 500 milliseconds, not the resource.
- **Fail criteria:** Any API endpoint that returns records from multiple tenants in a single response. An endpoint with a tenant filter in the query but not enforced at the database layer does not count as pass. Any single-resource endpoint that returns a resource without verifying the requesting user's tenant owns it. Any endpoint that accepts a `tenantId` in the request body or query string and uses that value without validating it matches the authenticated session's tenant.
- **Skip (N/A) when:** No API routes are detected. Signal: no `app/api/` directory, no `pages/api/` directory, no serverless function files, no tRPC or GraphQL server setup.
- **Detail on fail:** Name the specific endpoints and what they expose. Example: `"GET /api/projects returns all projects regardless of tenant. GET /api/documents/:id returns document for any tenant if ID is known — no ownership check found in src/app/api/documents/[id]/route.ts"`
- **Cross-reference:** Compare with `saas-multi-tenancy.tenant-boundaries.cross-tenant-search-blocked` — API responses (this check) and search results (cross-tenant-search) both need tenant scoping, but this check focuses on direct API access.
- **Remediation:** For list endpoints, the safest fix is to ensure the database query includes the tenant scope (see the every-query-tenant-scoped check). For single-resource lookups, always fetch by both `id` AND `tenantId` together:

  ```ts
  // Instead of:
  const doc = await db.documents.findUnique({ where: { id: params.id } })

  // Do this:
  const doc = await db.documents.findUnique({
    where: { id: params.id, organizationId: session.user.organizationId }
  })
  if (!doc) return new Response('Not found', { status: 404 })
  ```

  Never fetch a resource then check ownership after — fetch with ownership as part of the query predicate so the database returns nothing if there's a mismatch.

---

## External references

- cwe CWE-285
- cwe CWE-639
- owasp A01
- nist AC-4
- gdpr Art. 33

Taxons: access-control

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