IDOR (Insecure Direct Object Reference) in multi-tenant APIs is CWE-639 and CAPEC-122: an attacker increments or guesses a resource ID and retrieves a resource that belongs to a different tenant. The assumption that a valid UUID implies authorization is the root cause — valid IDs are observable (from your own data, from shared links, from support interactions) and UUIDs are not as unguessable as developers assume when millions exist. OWASP A01 classifies IDOR as one of the most prevalent and impactful web vulnerabilities.
High because resource IDs can be observed through normal product usage, making cross-tenant access exploitable by any authenticated user without brute force.
Include the tenant scope directly in the database lookup predicate so the database enforces ownership — never fetch then check:
// Vulnerable — fetches any tenant's document:
const doc = await db.document.findUnique({ where: { id: params.id } })
if (!doc) return notFound()
// Secure — returns nothing if the doc belongs to a different tenant:
const doc = await db.document.findFirst({
where: {
id: params.id,
organizationId: session.user.organizationId
}
})
if (!doc) return new Response('Not found', { status: 404 })
Use findFirst with both conditions rather than findUnique with just the ID. Apply this pattern in every [id] dynamic route under src/app/api/.
ID: saas-multi-tenancy.tenant-boundaries.cross-tenant-search-blocked
Severity: high
What to look for: Examine API endpoints that accept a resource identifier in the URL path (e.g., /api/projects/:projectId, /api/documents/:docId, /api/users/:userId). For each such endpoint, verify that the handler does not assume that a valid resource ID implies authorization — it must also verify the resource belongs to the requesting tenant. Look for patterns where the handler fetches the resource by ID only, then uses it without checking tenant ownership.
Pass criteria: Enumerate all resource-by-ID endpoints and verify at least 100% confirm that the resource belongs to the authenticated user's tenant before processing the request. The ownership check must happen before any business logic executes. A valid resource ID belonging to a different tenant should return 403 or 404.
Fail criteria: Any endpoint that fetches a resource by ID and uses it without checking that the resource's tenant matches the requesting session's tenant. Insecure Direct Object Reference (IDOR) vulnerability where guessing/enumerating a resource ID grants access to another tenant's resource.
Skip (N/A) when: No resource-by-ID API endpoints are detected. Signal: no parameterized route files in app/api/, no [id] or similar dynamic segments in API routes.
Detail on fail: Name the vulnerable endpoints. Example: "GET /api/documents/[id]/route.ts fetches document by ID without verifying the document's organizationId matches the session user's organizationId. Any authenticated user can access any document by knowing its ID."
Remediation: Always include the tenant scope in the lookup:
// Vulnerable:
const doc = await db.document.findUnique({ where: { id: params.id } })
if (!doc) return notFound()
// Secure:
const doc = await db.document.findFirst({
where: {
id: params.id,
organizationId: session.user.organizationId // ownership check in the query
}
})
if (!doc) return new Response('Not found', { status: 404 })
Use findFirst with both conditions rather than findUnique with just the ID. The database returns nothing on mismatch, so the handler never has the opportunity to use the wrong-tenant resource.