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 (0)