Change the User ID in Your API Request: Finding IDOR Before Users Do
· Fortus Team · 5 min read
Here is the shortest security test in web development: log in, open your own data, change the ID in the request, and see what comes back. Picture this: your orders live at an endpoint with your user number in the path. You swap one digit, replay the request, and stare at someone else's order history. Here's a scenario I keep seeing: the builder only ever tested with one account, so the missing ownership check never had a chance to fail until a curious user ran the same experiment in production. This bug has a name — insecure direct object reference, or IDOR — and it is arguably the most common vulnerability in AI-generated apps. The generator builds working CRUD, wires it to the logged-in session, and stops. The query fetches by the requested ID without also constraining to the requester. It works perfectly for every legitimate user, which is why it ships. Authorization is not authentication: proving who you are says nothing about which objects you may touch.
Why AI-generated CRUD keeps producing this
The vulnerable pattern is almost always the same line shape. A route handler reads an ID from the URL, a query parameter, or the request body, then queries the database by that ID alone. Select the order where the id matches. Return it. No ownership predicate, no middleware asserting the resource belongs to the caller. Sequential integer ids make the consequences worse. When object ids are predictable — 123, 124, 125 — enumeration is trivial, and one swapped digit explores the whole table. Opaque random ids slow casual browsing but fix nothing on their own, because the endpoint still answers whoever asks. Only a server-side ownership check closes the hole: constraining the query to rows owned by the authenticated user, middleware that loads the resource and compares owner fields before the handler runs, or a database policy that enforces the same rule for every query path. Client-side hiding is not a fix either. Removing the link from the UI, checking ownership only in frontend code, or trusting a hidden field still leaves the endpoint answering direct requests. The check must run on the server, on every handler that loads a resource by a request-supplied id.
Where to look in your codebase
IDOR hides in route handlers, server actions, and data-access helpers, so the review is a trace from request to query. In Next.js apps, inspect API routes, dynamic segments, and server actions for handlers that read an id from params, search params, or the body. In Express-style backends, inspect route controllers and middleware chains. In Python backends, inspect view and handler functions with ORM queries. Everywhere, the question is identical: does the query that loads the object also constrain by the authenticated user, or does an ownership assertion run before data is returned. Also inspect your schema for sequential primary keys exposed in URLs or responses, and check whether database-level policies cover the tables in question. Already-handled code shows its work: an explicit owner predicate in the query, an assertion comparing the resource owner against the session user, or a policy enforcing per-owner access with a comment marking intentionally public resources. Anything less is the finding, and each finding needs both halves cited — where the id enters from the request, and the query that uses it unchecked.
A manual self-test with two users
Here is a concrete self-test you can run manually on your own app today. Create two accounts, user A and user B, each owning at least one private object — an order, a document, a profile, a project. Log in as A and record the ids of A's objects from normal use. Log in as B in a separate session and replay the same requests with A's ids substituted: the URL path, the query parameter, the body field, whichever your app uses. Try reads first, then writes and deletes if your app exposes them. If B's session returns or modifies A's data anywhere, that endpoint lacks an ownership check. Then fix at the query: add the owner constraint alongside the id lookup, or add middleware asserting resource ownership before the handler runs. Prefer opaque ids for user-facing references as defense in depth, and consider database-level owner policies so every query path inherits the rule. Re-run the two-user swap until every endpoint answers only its owner. Cover every route that accepts an id — IDOR is a per-handler bug, and one fixed endpoint says nothing about its neighbors.
Where a file scanner helps
A source-file scanner fits IDOR review because both halves of the evidence live in code: the id entering from the request and the query running without an ownership predicate. It can trace route handlers to their queries, flag sequential keys exposed to users, and recognize already-handled patterns — owner predicates, ownership middleware, covering policies — so clean endpoints are not flagged. External authorization layers outside the repo, like gateway rules or dashboard settings, stay beyond what files can prove, so an unprotected-looking handler with possible external coverage deserves a noted assumption rather than a dismissal. Treat the scanner as the exhaustive tracer across every route, and the two-user swap as the proof that the enforcement holds at runtime.
Find missing ownership checks before users do
Fortus analyzes your API route handlers and database queries to flag IDOR vulnerabilities with exact file and line citations and ready-to-paste fix prompts.
or learn more about Fortus →