IDOR in AI-Generated APIs: The Ownership Check Cursor Always Skips A developer discovered that AI code generators like Cursor consistently omit ownership checks in API endpoints, leading to Insecure Direct Object Reference (IDOR) vulnerabilities. The AI correctly implements authentication middleware but fails to verify that the authenticated user owns the requested resource, a pattern traced back to training data that focuses on authentication rather than authorization. The developer recommends adding a simple ownership check after every resource fetch and using tools like semgrep or SafeWeave to catch the pattern before deployment. findById call fixes the entire patternI reviewed a friend's side project last month. Solid app -- JWT auth, protected routes, refresh token rotation. Then I ran a quick test: logged in as User A, grabbed a document ID from the URL, opened a private tab as User B, and requested the same endpoint. User A's data came back clean. He'd built the whole backend in Cursor. The AI had done a genuinely good job with authentication -- middleware, token validation, all wired up correctly. But every single resource endpoint had the same gap: it checked whether you were logged in. It never checked whether the resource belonged to you . That's CWE-639. Authorization Bypass Through User-Controlled Key. OWASP Top 10, A01: Broken Access Control. And AI code generators reproduce it at scale. Here's what Cursor generates for a document endpoint: // CWE-639: authenticated but no ownership check app.get '/api/documents/:id', authenticate, async req, res = { const doc = await Document.findById req.params.id ; if doc return res.status 404 .json { error: 'Not found' } ; res.json doc ; } ; Authentication passes. The route looks protected. But swap :id for any valid document ID in the database and you get the data -- regardless of who owns it. Change the number. Get the record. That's IDOR. The same pattern shows up in PUT and DELETE routes. Cursor wires up authenticate correctly every time. It skips the one line that makes the route actually private. I've seen this pattern in payment record endpoints, private message threads, medical note APIs. The auth middleware is there. The access control is not. The reason is boring. AI models train on tutorials, StackOverflow answers, and open-source repos. Most of that code is written to teach how authentication works -- JWT validation, session middleware, token refresh. It demonstrates the concept correctly. What tutorial code almost never models: the ownership check after the fetch. That's assumed to be obvious. It's left as an exercise. The post is about JWTs, not about who owns the document. The model learned the template. It didn't learn the gap. One check. After every resource fetch: // Fixed: ownership validated after fetch app.get '/api/documents/:id', authenticate, async req, res = { const doc = await Document.findById req.params.id ; if doc || doc.userId.toString == req.user.id { return res.status 403 .json { error: 'Forbidden' } ; } res.json doc ; } ; Two notes on this: Return 403, not 404. Returning 404 when "the document exists but isn't yours" leaks less about what IDs exist. Some teams prefer it. Either way, the ownership check is what matters. For larger codebases, a policy layer CASL, Casbin, or a simple assertOwnership doc, req.user helper is cleaner than repeating this inline everywhere. But even the raw version above eliminates the bug class entirely. A quick semgrep rule or grep for findById without an ownership check in the same function scope will surface every unprotected endpoint in a codebase. Takes about 15 seconds to run. I've been running SafeWeave https://safeweave.dev 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 targeting findById without ownership assertions will catch most of what's in this post. The important thing is catching it before it ships, whatever tool you use.