Why Cursor Writes IDOR Into Your API Routes (CWE-639)
AI editors add a login check to your API routes but skip the ownership check, so any logged-in user can read another user's data by changing the ID in the URL. Here is the one-line fix for IDOR (CWE-639).
SafeWeave Team
TL;DR
- AI editors add a login check to your API routes but skip the ownership check, so any logged-in user can read another user's data by changing the ID in the URL (CWE-639, IDOR).
- It happens because tutorials treat "authenticated" as if it means "authorized," and the AI learned from those tutorials.
- The fix is one line: scope every lookup to the current user instead of trusting a raw ID from the request.
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 Vulnerable Code
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.
Catch these vulnerabilities automatically with SafeWeave
SafeWeave runs 8 security scanners in parallel — SAST, secrets, dependencies, IaC, containers, DAST, license, and posture — right inside your AI editor. One command, zero config.
Start Scanning FreeWhy This Keeps Happening
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.
Try SafeWeave in 30 seconds
npx safeweave-mcp
Works with Cursor, Claude Code, Windsurf, and VS Code. No signup required for the free tier — 3 scanners, unlimited scans.
The Fix
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.
FAQ
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 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.
Secure your AI-generated code with SafeWeave
8 security scanners running in parallel, right inside your AI editor. SAST, secrets, dependencies, IaC, containers, DAST, license compliance, and security posture — all in one command.
No credit card required · 3 scanners free forever · Runs locally on your machine