Why Cursor Writes IDOR Into Your API Routes (CWE-639) A developer discovered that Cursor, an AI code editor, generated an API endpoint vulnerable to Insecure Direct Object Reference (IDOR, CWE-639). The endpoint authenticated the user but failed to verify ownership of the requested invoice, allowing any logged-in user to access another user's private data by changing the ID in the URL. The developer fixed the issue by scoping the database query to the authenticated user's ID. I asked Cursor to build an endpoint that returns an invoice by ID. It gave me clean code. Auth middleware on the route, a database lookup, a JSON response. It ran on the first try. Then I logged in as a different test user and changed the number at the end of the URL. Invoice 1001 belonged to someone else. I got the whole thing back: amount, line items, billing address. No error, no warning. Just another user's private data on my screen. That is IDOR, an Insecure Direct Object Reference, and it is one of the most common holes I find in AI-generated APIs. The frustrating part is that the code looks secure. It even has an auth check. It just checks the wrong thing. The endpoint below is broken because it confirms you are logged in but never confirms the invoice is yours. findById takes the ID straight from the URL and returns whatever it finds. // CWE-639: authenticated, but no ownership check app.get '/api/invoices/:id', authenticate, async req, res = { const invoice = await Invoice.findById req.params.id ; res.json invoice ; } ; The authenticate middleware does its job. It proves the request comes from a real, logged-in user. What it does not prove is that this particular user has any right to invoice :id . Change the ID, get someone else's record. Increment it in a loop and you can walk the entire table. AI editors confuse authentication with authorization because almost every tutorial they trained on does the same thing. Authentication is "who are you." Authorization is "are you allowed to touch this specific object." Most example code stops at the first one. When a tutorial demonstrates a "get resource by ID" route, the shortest version that runs is Model.findById req.params.id . That snippet appears thousands of times in the training data, almost always without an ownership filter, because the tutorial's goal is to show routing and database access, not object-level access control. The model reproduces the pattern it saw most. It even adds the authenticate middleware, because that is a visible, nameable step. Ownership is invisible. It lives in the shape of the query, and there is no keyword for the AI to copy. Scope the lookup to the authenticated user so the database can only ever return records that belong to them. Never fetch by a raw ID from the request and trust it. // Fixed: the query itself enforces ownership app.get '/api/invoices/:id', authenticate, async req, res = { const invoice = await Invoice.findOne { id: req.params.id, userId: req.user.id, } ; if invoice return res.status 404 .json { error: 'Not found' } ; res.json invoice ; } ; The change is small but the logic is different. The query now says "find this invoice and confirm it is mine" in a single step. If the ID belongs to another user, the database returns nothing and the caller gets a 404, which also avoids confirming that the record exists at all. The same rule holds in Python. Filter by owner in the query, do not fetch first and check later. Fixed FastAPI + SQLAlchemy @app.get "/api/invoices/{invoice id}" def get invoice invoice id: int, user=Depends current user , db=Depends get db : invoice = db.query Invoice .filter Invoice.id == invoice id, Invoice.user id == user.id .first if invoice is None: raise HTTPException status code=404, detail="Not found" return invoice Do this on every route that reads or writes a specific object: invoices, orders, messages, uploaded files, profile settings. Anywhere an ID comes from the client, the query has to prove ownership, not just existence. Q: Isn't login enough to protect an API route? A: No. Login proves who the user is. It says nothing about whether that user owns the specific record they are requesting. You need an ownership check on every object-level route in addition to authentication. Q: Do UUIDs instead of sequential IDs fix IDOR? A: No. Random UUIDs make IDs harder to guess, but that is obscurity, not access control. If an ID leaks in a URL, log, or referrer header, the record is still exposed. Always enforce ownership in the query regardless of ID format. Q: How do I find IDOR in code I already shipped? A: Look for any route that reads an ID from the request and calls findById , get , or a raw query without filtering by the current user. Every one of those is a candidate. Testing is simple: log in as user A, request user B's IDs, and see what comes back. I've been running SafeWeave https://tinyurl.com/237e23ht for this. It hooks into Cursor and Claude Code as an MCP server, and its posture and SAST scanners flag routes that fetch an object by a request-supplied ID without an ownership filter, before I move on to the next file. Even a careful code-review checklist that asks "does this query prove ownership, not just existence?" will catch most of what's in this post. The important thing is catching it early, whatever tool you use.