IDOR (Insecure Direct Object Reference) — CWE-639, CAPEC-143 — is one of the most reliably exploitable authorization failures: an attacker supplies someone else's resource ID and receives that resource. OWASP A01 (Broken Access Control) names IDOR as a primary example. The threat model is simple: authenticated user A calls GET /api/invoices/invoice-uuid-owned-by-user-B and receives user B's invoice. UUIDs reduce guessability but are not authorization controls — they merely slow brute-force enumeration. NIST 800-53 AC-3 requires access enforcement based on identity, not identifier obscurity. A single IDOR-vulnerable endpoint exposes every record in the affected table to any authenticated caller.
Critical because the attack requires only authentication and knowledge of one valid ID — no privilege escalation, no chaining, just a direct read or write of another user's data.
The fix is identical to resource ownership verification: include the user's ID in every query that fetches by a client-supplied ID. This applies whether the ID is in the URL path, a query parameter, or the request body.
// Pattern for every route with [id]:
const resource = await db.invoice.findFirst({
where: { id: params.id, userId: session.user.id }
});
if (!resource) return NextResponse.json({ error: 'Not found' }, { status: 404 });
For cross-resource access (user has access through their organization), verify via the organizational membership table — db.orgMember.findFirst({ where: { orgId: resource.orgId, userId: session.user.id } }) — rather than checking direct ownership.
ID: saas-authorization.resource-auth.no-idor
Severity: critical
What to look for: Examine all API routes with dynamic route segments ([id], [postId], [userId], [orderId]). For each, trace how the ID is used. Does it flow directly into a database query without being checked against the requesting user's identity? Also look for IDs passed as query parameters (?id=123) and in request bodies. This overlaps with resource-ownership-verified but specifically focuses on the pattern from the attacker's perspective: can user A access user B's resource by guessing or iterating IDs?
Pass criteria: No endpoint allows access to resources owned by a different user by supplying a valid but non-owned resource ID. At least 1 implementation must be verified. The combination of authentication + ownership check prevents cross-user resource access.
Fail criteria: Any endpoint returns or modifies data for a resource ID that does not belong to the requesting user. This is true regardless of whether sequential IDs are used (guessable) or UUIDs (harder to guess but still an authorization failure).
Skip (N/A) when: All resources returned by ID are intentionally public (no per-user ownership concept). Skip individual checks where the route explicitly handles public resources.
Detail on fail: "IDOR risk: endpoint [route] uses client-supplied ID without verifying the requesting user owns that resource. UUIDs do not substitute for authorization checks." (List all affected routes.)
Remediation: The fix for IDOR is the same as resource ownership verification — always include the user's ID in the database query. Using UUIDs instead of sequential integers is a defense-in-depth measure, not an authorization control. An attacker who has one UUID can still use it to access the resource if ownership is not checked.
// Pattern for every route with [id]:
const resource = await db.invoice.findFirst({
where: { id: params.id, userId: session.user.id }
});
if (!resource) return NextResponse.json({ error: 'Not found' }, { status: 404 });
For cross-resource relationships (e.g., "does user have access to this invoice through their organization?"), verify the relationship through the organizational membership table rather than direct ownership.