Fetching a resource by a client-supplied ID without verifying the requesting user owns it is Insecure Direct Object Reference — CWE-639 (Authorization Bypass Through User-Controlled Key). Any user who knows (or guesses) a resource ID can read or modify records they do not own. OWASP A01 (Broken Access Control) lists IDOR as a primary failure mode. UUIDs do not fix this: they are harder to guess but are not authorization controls. NIST 800-53 AC-3 requires that access enforcement be based on identity and authorization, not obscurity of identifiers. A missed ownership check on a single endpoint exposes the data of every user who has a record in that table.
Critical because any authenticated user can access any record in the affected table by supplying a valid ID, regardless of who owns it.
Include ownership in the database query itself — making the check atomic with the fetch and impossible to accidentally omit. Return 404 (not 403) to avoid leaking whether the resource exists.
// Safe — ownership enforced at query level
const post = await db.post.findFirst({
where: {
id: params.id,
authorId: session.user.id, // Ownership is part of the lookup
},
});
if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 });
// 404 (not 403) avoids confirming the resource exists to unauthorized callers
For shared resources accessed through organizational membership, verify the relationship through a membership table rather than relying on direct ownership.
ID: saas-authorization.resource-auth.resource-ownership-verified
Severity: critical
What to look for: Count all relevant instances and enumerate each. Find database queries that fetch a single resource by a user-supplied ID — findUnique({ where: { id: params.id } }), findFirst({ where: { id: body.id } }), raw SQL WHERE id = $1. Check whether these queries also include a filter on the authenticated user's ID or a membership verification. The pattern findUnique({ where: { id: params.id } }) followed by if (resource.userId !== session.user.id) is acceptable. The pattern findUnique({ where: { id: params.id } }) with no subsequent ownership check is a fail.
Pass criteria: All queries that retrieve a single resource by a client-supplied ID either (a) include the user's ID in the WHERE clause as part of the query, or (b) check the returned resource's ownership field immediately after fetching and return 403/404 if ownership does not match. At least 1 implementation must be verified.
Fail criteria: Any query fetches a resource by client-supplied ID without verifying the requesting user's relationship to that resource — either inline in the query or immediately after.
Skip (N/A) when: The project has no routes that accept resource IDs as parameters. Skip for genuinely public resources (blog posts, public profiles) where all authenticated users are intended to have read access.
Detail on fail: "Found N query/queries that fetch resources by ID without ownership verification. Any user who knows a resource ID can read/modify it." (List the specific files and queries.)
Remediation: Include ownership in the database query itself — this is the safest approach because the check is atomic with the fetch.
// Unsafe — fetches by ID alone
// const post = await db.post.findUnique({ where: { id: params.id } });
// Safe — fetches only if the requesting user owns it
const post = await db.post.findFirst({
where: {
id: params.id,
authorId: session.user.id, // Ownership enforced at query level
},
});
if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 });
// Return 404 (not 403) to avoid revealing whether the resource exists
Returning 404 instead of 403 for unauthorized resource access is intentional — it avoids leaking information about resource existence to unauthorized callers.