TL;DR
- Cursor generates authenticated API routes with no ownership verification by default
- This creates IDOR (CWE-639) -- any logged-in user can read or delete any other user's data
- Fix is 3 lines: check
resource.userId === req.user.idbefore returning anything
I was reviewing a friend's side project last week. TypeScript, Prisma, built almost entirely in Cursor. Clean code, good structure, auth middleware on every route.
I asked him to pull up his /api/documents/:id endpoint.
No ownership check. User 42 could request /api/documents/1 and get user 1's private documents back with a 200. No error. No log. Just data. He had no idea -- Cursor never flagged it.
The Vulnerable Code Pattern (CWE-639)
Prompt Cursor with "create a GET endpoint to fetch a document by ID" and you'll get something like this:
// ❌ CWE-639: No ownership verification
app.get('/api/documents/:id', authenticate, async (req, res) => {
const doc = await prisma.document.findUnique({
where: { id: req.params.id }
});
if (!doc) return res.status(404).json({ error: 'Not found' });
res.json(doc);
});
The authenticate middleware is there. JWT is verified. But there's no check that doc.userId === req.user.id.
Any authenticated user can iterate IDs -- /api/documents/1, /api/documents/2 -- and pull every record in the database. That's a textbook IDOR (Insecure Direct Object Reference).
Why AI Keeps Writing This
AI code assistants are trained on millions of tutorials and StackOverflow answers. Most of those examples show authentication. Almost none show authorization.
Authentication answers "are you who you claim to be?" Authorization answers "are you allowed to access this specific resource?" The second check is nearly absent from tutorial code -- tutorials demonstrate concepts, not production security patterns.
Cursor, Claude Code, Copilot -- they all reproduce this gap faithfully. When you prompt for a CRUD endpoint, the model generates code that satisfies the prompt. Ownership checking isn't in the prompt, so it doesn't appear in the output.
The Fix -- 3 Lines
// ✅ Ownership check added
app.get('/api/documents/:id', authenticate, async (req, res) => {
const doc = await prisma.document.findUnique({
where: { id: req.params.id }
});
if (!doc) return res.status(404).json({ error: 'Not found' });
// The three lines that matter:
if (doc.userId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
res.json(doc);
});
A few things worth noting:
Return 403, not 404. Some teams prefer 404 to avoid leaking that the resource exists. Both are defensible -- pick one and stay consistent across your API.
Don't just filter in the query. Adding userId: req.user.id to the where clause silently returns null instead of throwing 403. It works, but it makes debugging harder and breaks explicit logging. Make intent clear.
Check before business logic runs. If anything happens between fetching and returning the resource, gate on ownership first.
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 semgrep rule watching for findUnique calls without an ownership comparison will catch most of what's in this post. The important thing is catching it early, whatever tool you use.
Top comments (0)