DEV Community

Cover image for Cursor Keeps Writing IDOR Into Your APIs. Here's the Fix.
Charles Kern
Charles Kern

Posted on

Cursor Keeps Writing IDOR Into Your APIs. Here's the Fix.

TL;DR

  • AI editors generate authenticated endpoints with no ownership verification
  • Any valid JWT holder can read any other user's data by guessing an ID
  • Fix: scope the query to the requesting user, or check ownership immediately after fetch

I've been reviewing side-project codebases for the past few weeks. The stack varies -- Express, FastAPI, Rails, doesn't matter. The bug is always the same.

Someone asked Cursor or Claude Code to add an API endpoint to fetch user data. The AI wrote working code. Correct query, correct response shape, even a 404 handler. What it skipped was the line that checks whether the person asking actually owns the record they're asking for.

This is IDOR -- Insecure Direct Object Reference. CWE-862. OWASP Top 10. The reason it keeps showing up in AI-generated code isn't random.

The Vulnerable Endpoint

Here's the pattern:

// CWE-862 -- Missing Authorization
app.get('/api/documents/:id', authenticateToken, async (req, res) => {
  const doc = await Document.findById(req.params.id);
  if (!doc) return res.status(404).json({ error: 'Not found' });
  res.json(doc);
});
Enter fullscreen mode Exit fullscreen mode

The middleware runs. The user is authenticated. But the document goes back to whoever asked, regardless of whether they own it. Authenticated but unauthorized. That's the gap.

Exploiting it is trivial. Open the network tab, grab any document ID from your own responses, increment it by 1, send the same request. If the server responds with another user's document, you've found the bug.

Why AI Gets This Wrong

The training data problem is straightforward. Tutorials that demonstrate "how to build a REST API" don't include authorization logic. They prove the concept. The code gets the data and returns it. That's enough to teach the pattern.

When you ask the AI to "add an endpoint that returns a document by ID", it does exactly that. It doesn't anticipate that an authenticated attacker will probe IDs they don't own. The happy path works. The adversarial path is invisible to it.

The Fix

Two lines added immediately after the fetch:

// Fixed -- ownership check before response
app.get('/api/documents/:id', authenticateToken, async (req, res) => {
  const doc = await Document.findById(req.params.id);
  if (!doc) return res.status(404).json({ error: 'Not found' });
  if (doc.userId.toString() !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  res.json(doc);
});
Enter fullscreen mode Exit fullscreen mode

Or scope the query from the start so the check is structural:

const doc = await Document.findOne({ _id: req.params.id, userId: req.user.id });
Enter fullscreen mode Exit fullscreen mode

Same fix across frameworks. Django: Document.objects.get(pk=id, user=request.user). FastAPI: inject current_user and filter by owner. Rails: current_user.documents.find(params[:id]).

The rule is simple: never return a user-scoped resource fetched by ID alone.

Where to Catch It

Audit every route that accepts an :id or ?id= parameter and returns a database record. Count the ownership checks. Any route missing one is an IDOR candidate.

A semgrep one-liner:

semgrep --config=r/owasp.idor . --include="*.js" --include="*.py"
Enter fullscreen mode Exit fullscreen mode

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)