Broken Object Level Authorization (OWASP API Security Top 10 2023 API1) is the single most exploited API vulnerability class. Authentication confirms who you are; object-level authorization confirms what you can touch. Without it, any authenticated user can substitute another user's ID in a URL and retrieve or modify that user's data — orders, messages, payment methods, medical records. CWE-639 (Authorization Bypass Through User-Controlled Key) describes exactly this pattern: the key is user-supplied, the backend trusts it. Real-world incidents like the Optus breach involved straightforward IDOR on predictable object IDs.
Critical because any authenticated attacker with a valid session can exfiltrate or overwrite other users' private data by incrementing or guessing resource IDs.
After authenticating the user, always fetch the resource and compare its owner field against the authenticated user's ID before returning or modifying it. Never trust a resource ID from the URL without verifying ownership server-side.
// src/app/api/orders/[id]/route.ts
export async function GET(req: Request, { params }: { params: { id: string } }) {
const user = await getAuthUser(req)
if (!user) return new Response('Unauthorized', { status: 401 })
const order = await db.order.findUnique({ where: { id: params.id } })
if (!order) return new Response('Not Found', { status: 404 })
// Ownership check — do this before returning any data
if (order.userId !== user.id) return new Response('Forbidden', { status: 403 })
return Response.json(order)
}
Apply the same pattern to PATCH, PUT, and DELETE handlers.
ID: api-security.auth.object-level-access-control
Severity: critical
What to look for: Enumerate every relevant item. Check whether API endpoints that return or modify user-specific data verify that the requesting user owns the resource. Look for patterns where a user ID is extracted from the URL or parameters, and then the endpoint checks that the authenticated user's ID matches before returning or updating data.
Pass criteria: At least 1 of the following conditions is met. Endpoints returning user-specific data (e.g., GET /api/users/{id}, PATCH /api/posts/{postId}) verify that the authenticated user matches the resource owner before proceeding. Before evaluating, extract and quote the relevant configuration or code patterns found. Report the count of items checked even on pass.
Fail criteria: Endpoints accept any resource ID and return or modify data belonging to other users. For example, any authenticated user can call GET /api/users/123 and receive data for user 123, even if the requesting user is user 456.
Do NOT pass when: The item exists only as a placeholder, stub, or TODO comment — partial implementation does not count as passing.
Skip (N/A) when: The API only returns public data or data that all authenticated users are allowed to access (no user-specific resource isolation).
Cross-reference: For deployment and infrastructure concerns, the Deployment Readiness audit covers production configuration.
Detail on fail: Describe the vulnerable endpoints. Example: "GET /api/profile/{userId} returns user data for any user ID without verifying ownership — users can access other users' profiles" or "PATCH /api/orders/{orderId} allows any authenticated user to modify any order""
Remediation: Add an ownership check before returning or modifying user-specific resources:
export default authMiddleware(async (req, res, user) => {
const { id } = req.query
const post = await db.post.findUnique({ where: { id } })
// Check ownership
if (post.authorId !== user.id) {
return res.status(403).json({ error: 'Forbidden' })
}
res.json(post)
})