DEV Community

Cover image for AI-Generated APIs Have an IDOR Problem: 3 Patterns Cursor Misses
Charles Kern
Charles Kern

Posted on

AI-Generated APIs Have an IDOR Problem: 3 Patterns Cursor Misses

TL;DR

  • AI editors add authentication middleware but skip per-resource ownership checks by default
  • Any logged-in user can access another user's data by guessing or incrementing an ID
  • Fix: scope ownership into the DB query -- one line before you ship, not after

I was reviewing a side project built almost entirely with Cursor. Clean code. Tests passing. Already deployed.

Then I spotted this:

app.get('/api/documents/:id', authenticateToken, async (req, res) => {
  const doc = await Document.findById(req.params.id);
  res.json(doc);
});
Enter fullscreen mode Exit fullscreen mode

The authenticateToken middleware ran. So the route looked protected. But there was no check that req.user.id === doc.userId. Any authenticated user could read any document by guessing the ID. That's IDOR. CWE-862. And it's in almost every AI-generated API I've reviewed.

Why AI editors get this wrong

AI tools are trained on tutorials and StackOverflow answers. Tutorials teach authentication -- "add this middleware and your route is secure." They almost never teach authorization -- "now verify the authenticated user is allowed to access this specific record."

The model learns: authenticateToken = protected. It doesn't learn the follow-up. I've seen this in Cursor output, Claude Code output, and Copilot suggestions. All three get authentication right. All three miss authorization.

The three patterns to watch for

Pattern 1 -- No ownership check at all (CWE-862)

// ❌ CWE-862: Missing Authorization
app.get('/api/orders/:id', authenticateToken, async (req, res) => {
  const order = await Order.findById(req.params.id);
  if (!order) return res.status(404).json({ error: 'Not found' });
  res.json(order);
});

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

Pattern 2 -- Delete endpoints are worse

// ❌ Any authenticated user can delete any record
app.delete('/api/posts/:id', authenticateToken, async (req, res) => {
  await Post.findByIdAndDelete(req.params.id);
  res.json({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

No check who owns the post. The AI generated working code. It's just wrong.

Pattern 3 -- Related resource fetches without scope

// ❌ Returns comments for any post, regardless of ownership
app.get('/api/posts/:id/comments', authenticateToken, async (req, res) => {
  const comments = await Comment.find({ postId: req.params.id });
  res.json(comments);
});
Enter fullscreen mode Exit fullscreen mode

This one is subtle. The endpoint itself might be intentionally public. But in a private notes app? It's data exposure by default.

The better fix -- bake ownership into the query

Don't fetch first, check second. Instead, filter at the DB level:

// Ownership enforced at query time -- not application logic
const doc = await Document.findOne({
  _id: req.params.id,
  userId: req.user.id
});
if (!doc) return res.status(404).json({ error: 'Not found' });
Enter fullscreen mode Exit fullscreen mode

Two advantages. First, you get 404 instead of 403 -- you don't leak whether the resource exists at all. Second, even if your application-level check has a bug, the DB query won't return the wrong user's data.

What to tell your AI editor

If you're using Cursor or Claude Code, add this to your system prompt or rules file:

After every resource fetch, check that req.user.id matches the resource's owner field. Use findOne with ownership in the query filter, not findById followed by a check. Return 404, not 403.

It works. The model follows explicit rules better than it infers them from context.

I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags CWE-862 patterns before I move on. That said, a pre-commit semgrep rule scanning for findById calls not followed by an ownership comparison catches most of what's in this post. Catch it early, whatever your tool.

Top comments (0)