DEV Community

Cover image for IDOR in AI-Generated APIs: What Cursor Won't Check for You
Charles Kern
Charles Kern

Posted on

IDOR in AI-Generated APIs: What Cursor Won't Check for You

TL;DR

  • AI editors generate routes that fetch resources by ID with no ownership check -- classic IDOR (CWE-639)
  • The pattern is everywhere in vibe-coded apps: any authenticated user can read any other user's data
  • One extra condition in the DB query fixes it -- the problem is AI doesn't add it unless you ask

I reviewed a side project last month. Node/Express backend, Cursor-generated, clean structure, well-commented. The developer was proud of their auth setup -- JWT tokens, bcrypt passwords, protected routes. Proper stuff.

Then I hit /api/orders/1. Logged in as user 847, I got back user 1's order. All of it. Name, address, items, total. Switched to /api/orders/2. Same result. The API was authenticated -- you needed a valid JWT to reach it. But it didn't care whose JWT you had.

This is IDOR: Insecure Direct Object Reference. OWASP ranks it #1 in the API Security Top 10. And AI editors reproduce it on every resource endpoint they generate.

The Vulnerable Pattern

Here's what Cursor outputs when you prompt "an endpoint to fetch a user's order":

// CWE-639: Authorization Bypass Through User-Controlled Key
app.get('/api/orders/:id', authenticate, async (req, res) => {
  const order = await Order.findById(req.params.id);
  if (!order) return res.status(404).json({ error: 'Not found' });
  res.json(order);
});
Enter fullscreen mode Exit fullscreen mode

Authentication check: yes. Ownership check: no.

User 42 can hit /api/orders/99 and read user 99's full order. The only defense is knowing the right ID -- and order IDs are usually sequential integers or guessable UUIDs.

The AI isn't being careless. It completed the stated task: fetch an order by ID, require auth. The ownership check is implicit domain knowledge that didn't make it into the prompt.

Why This Keeps Happening

LLMs train on GitHub and Stack Overflow. The examples there show authentication patterns -- middleware, JWT verification, session checks. Ownership checks are rarer in tutorials because they're application-specific: the code needs to know your data model and your business rule ("a user can only access their own order").

AI doesn't know your data model unless you tell it. Most prompts don't include "and make sure the requesting user owns this resource." So the AI skips it -- not out of negligence, but because the constraint was never in the prompt.

The result: correctly authenticated, completely unauthorized.

The Fix

One extra condition in the DB query:

app.get('/api/orders/:id', authenticate, async (req, res) => {
  // Query with userId -- DB-level ownership check
  const order = await Order.findOne({
    _id: req.params.id,
    userId: req.user.id
  });
  if (!order) return res.status(404).json({ error: 'Not found' });
  res.json(order);
});
Enter fullscreen mode Exit fullscreen mode

This is better than a separate if-check for two reasons. First, no information leakage -- returning 404 instead of 403 means the caller can't confirm whether ID 99 exists and belongs to someone else. Second, ownership is enforced at the query level, so there's no way to forget to check after the fetch.

Audit Your Existing Routes

A quick grep surfaces the candidates:

grep -rn "req.params" src/ | grep "findById\|findOne"
Enter fullscreen mode Exit fullscreen mode

Any hit that doesn't include userId or req.user in the same query is a likely IDOR. Review each one manually.

For Express specifically: check every route using req.params.id, req.params.postId, req.params.orderId, and similar. If the query uses only the ID param and not the user ID, that endpoint probably has this bug. Check nested resources too -- /api/posts/:postId/comments/:commentId is two levels of ownership to verify.

I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags these patterns before I move on. That said, even a basic pre-commit hook with semgrep and gitleaks will catch most of what's in this post. The important thing is catching it early, whatever tool you use.

Top comments (2)

Collapse
 
henryaza profile image
Henry A

This is one of the most underreported risks of AI-assisted development. I've code-reviewed AI-generated APIs where every endpoint used the resource ID straight from the URL with zero ownership check — GET /api/invoices/:id returns any invoice to any authenticated user.

The pattern I enforce on every API I build or review:

1. Ownership scoping at the query layer, not the route layer

Don't check ownership after fetching. Scope the query itself:

// Bad — fetches first, checks after
const invoice = await Invoice.findById(req.params.id);
if (invoice.userId !== req.user.id) return res.status(403);

// Good — ownership is part of the query
const invoice = await Invoice.findOne({
  _id: req.params.id,
  userId: req.user.id
});
if (!invoice) return res.status(404);
Enter fullscreen mode Exit fullscreen mode

The second version never loads data the user doesn't own. The first version loads it into memory and trusts application logic to reject it — which is where bugs live.

2. Use opaque IDs instead of sequential integers

If your IDs are 1, 2, 3, an attacker iterates. UUIDs or NanoIDs don't prevent IDOR, but they eliminate casual enumeration. Defense in depth.

3. Middleware that AI won't generate for you

I add a scopeToUser middleware that injects { userId: req.user.id } into every database query by default. Any endpoint that intentionally needs cross-user access has to explicitly opt out — which makes it visible in code review. AI tools will never generate this pattern because it's architectural, not per-endpoint.

4. Automated IDOR scanning in CI

Write a test that creates two users, creates a resource as User A, and tries to access it as User B. Run it against every endpoint. This catches IDOR regressions automatically — especially important when AI is generating new endpoints weekly.

The core problem is that AI generates endpoint-by-endpoint, never thinking about cross-cutting concerns like authorization scoping. That's the human's job, and most teams aren't catching it in review.

Collapse
 
peacebinflow profile image
PEACEBINFLOW

The thing that quietly worries me isn't that AI skips ownership checks. It's that the resulting code looks complete. Auth middleware, proper HTTP status codes, error handling. It has all the surface markers of a well-built endpoint. A junior dev reviewing it might nod and move on, because nothing looks broken.

That's the new failure mode. Before AI, bad code often looked bad. Messy, incomplete, commented-out blocks, weird variable names. Now the dangerous code looks like it came from a tutorial. Clean. Confident. Wrong in a way that's invisible until someone malicious pokes it.

Your point about 404 vs 403 is the kind of subtlety that separates "works" from "secure." AI doesn't know the information-leakage implications of telling an attacker "that ID exists but it's not yours." That's not a coding pattern, it's a security intuition built from seeing how attackers actually probe systems.

Makes me wonder if the real shift here is that we used to learn security by breaking things—our own, our friends', our employers'. You'd ship a bug, someone would exploit it, you'd feel the sting, you'd never make that mistake again. If AI is generating the bug-free-looking-but-vulnerable code, and the developer never actually writes the vulnerable pattern themselves, does the lesson ever sink in? Are we outsourcing the bugs and accidentally outsourcing the learning too?