DEV Community

Cover image for The IDOR Bug Cursor Keeps Writing Into Your API Routes
Charles Kern
Charles Kern

Posted on

The IDOR Bug Cursor Keeps Writing Into Your API Routes

TL;DR

  • AI editors add authentication middleware but routinely skip ownership checks
  • Result: IDOR (CWE-639) -- any logged-in user can read or modify another user's data
  • Fix: one condition verifying resource.userId === req.user.id before returning data

I was doing a quick review of a side project a friend asked me to look at. Node.js backend, built mostly with Cursor. Clean code. Solid structure. Auth middleware on every route.

Then I noticed the orders endpoint.

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

Authenticated? Yes. Protected? Not really. Any logged-in user could hit /api/orders/1, /api/orders/2, and walk through every order in the database. This is IDOR -- Insecure Direct Object Reference (CWE-639). One of the most common vulnerabilities in web apps, and AI editors produce it constantly.

Why This Keeps Happening

Cursor, Claude Code, Copilot -- they're all trained on code that separates authentication from authorization. Authentication is the middleware layer. Authorization is the per-resource check. In tutorials and open-source examples, auth middleware is front-and-center. The ownership check is buried in business logic, varies wildly by app, or just isn't there at all.

The model sees the pattern: route + authenticate middleware = "protected route". It's not wrong. The route IS protected from unauthenticated requests. But protection from anonymous users is not the same as ensuring the authenticated user owns what they're requesting. The model completes what it learned. Ownership checks weren't part of that pattern in most training examples.

The Fix

One condition. That's all.

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' });

  // This is what Cursor forgets
  if (order.userId.toString() !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }

  res.json(order);
});
Enter fullscreen mode Exit fullscreen mode

For apps with admin roles, write an explicit helper -- isOwnerOrAdmin(req.user, resource) -- and call it on every parameterized route. Don't leave it for the AI to infer from context.

How to Find These Fast

Grep for route handlers that accept a URL parameter without a corresponding ownership check:

grep -rn "req.params\." src/routes/ | grep -v "userId\|req.user"
Enter fullscreen mode Exit fullscreen mode

Any match without an adjacent ownership check is a candidate for review. In a typical AI-generated backend, expect to find this pattern in 40-60% of parameterized routes.

I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags missing ownership checks via its posture scanner before the code moves on. That said, even a semgrep rule matching req.params without a subsequent req.user comparison will catch most of what's in this post. The important thing is catching it early, whatever tool you use.

Top comments (0)