No cross-tenant data visible in API responses
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:
// 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
tenantIdin 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, nopages/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
idANDtenantIdtogether:// 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 — Improper Authorization
- cwe · CWE-639 — Authorization Bypass Through User-Controlled Key
- owasp:2021 · A01 — Broken Access Control
- nist:rev5 · AC-4 — Information Flow Enforcement
- gdpr · Art. 33 — Notification of a personal data breach to the supervisory authority
Taxons
History
- 2026-04-18·v1.0.0·Initial import from saas-multi-tenancy·automated