DEV Community

Cover image for IDOR in Cursor-Generated APIs: The Auth Check That Never Shows Up
Charles Kern
Charles Kern

Posted on

IDOR in Cursor-Generated APIs: The Auth Check That Never Shows Up

TL;DR

  • Cursor and Claude Code generate resource endpoints that authenticate but never verify ownership
  • Any logged-in user can access any other user's data by iterating IDs
  • Three lines and a helper function fix the entire pattern across your API

I was reviewing a friend's SaaS side project last month. He'd built the whole backend using Cursor -- about 800 lines of Express routes over a week. Authentication worked. Database queries worked. Everything functioned correctly when he tested it himself.

Then I pulled his user ID from the JWT, swapped it into a request for a different user's invoice, and got back the full invoice data. CWE-639. Classic IDOR. Every single resource endpoint had it.

He hadn't made a mistake. Cursor had written exactly what he asked for -- endpoints that fetch resources by ID. The gap is that "fetch by ID" and "fetch by ID if the requester actually owns it" look identical from the outside until someone exploits them.

The Vulnerable Pattern

Here's what Cursor generates for a typical resource endpoint:

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

authenticateToken runs, so the user must be logged in. But there's no check that invoice.userId matches req.user.id. Any authenticated user can enumerate /api/invoices/1, /api/invoices/2, and work their way through your dataset.

PUT and DELETE endpoints generated in the same Cursor session have the same gap. The AI is consistent -- consistently wrong.

Why This Keeps Happening

The training data explanation is straightforward. Most tutorial code -- Stack Overflow answers, GitHub READMEs, blog posts about building REST APIs -- shows authentication as the final step. The concept of per-resource ownership is either implied ("obviously you'd check that") or skipped entirely because the tutorial is about something else: Mongoose syntax, Express routing, JWT setup.

AI models reproduce the patterns they're trained on. The pattern says: authenticate the user, fetch the resource, return it. Ownership check isn't in the pattern.

When I explicitly asked Cursor "does this endpoint check if the user owns the invoice?" it flagged the missing check immediately and added it. The model knows ownership matters. It just doesn't include the check by default because the examples it learned from don't either.

The Fix

Three lines. Add them to every endpoint that returns user-scoped data:

app.get('/api/invoices/:id', authenticateToken, async (req, res) => {
  const invoice = await Invoice.findById(req.params.id);
  if (!invoice) return res.status(404).json({ error: 'Not found' });
  if (invoice.userId.toString() !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  res.json(invoice);
});
Enter fullscreen mode Exit fullscreen mode

If you have more than a handful of routes, write a helper once:

async function findOwned(Model, id, userId) {
  const doc = await Model.findOne({ _id: id, userId });
  if (!doc) throw Object.assign(new Error('Not found or forbidden'), { status: 404 });
  return doc;
}
Enter fullscreen mode Exit fullscreen mode

Every route then becomes: const invoice = await findOwned(Invoice, req.params.id, req.user.id). The ownership check is baked into the query. No separate conditional, no chance of forgetting it in a new endpoint.

For the AI workflow side: add "always include ownership checks on resource endpoints" to your Cursor rules file or Claude Code custom instructions. A single-line reminder produces consistent results. Without it, you're relying on the model to infer ownership semantics from context -- which works sometimes but not reliably.

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 will catch most of what's in this post. The important thing is catching it early, whatever tool you use.

Top comments (0)