DEV Community

Cover image for IDOR in AI-Generated Code: The Auth Bug Cursor Keeps Missing
Charles Kern
Charles Kern

Posted on

IDOR in AI-Generated Code: The Auth Bug Cursor Keeps Missing

TL;DR

  • AI editors generate API endpoints with authentication but no ownership checks
  • A logged-in user can fetch any other user's data by changing one number in the URL
  • Fix: always validate req.user.id === resource.userId before returning data

I reviewed a side project last month. Node/Express backend, Cursor-generated, looked clean. Real auth middleware. JWT tokens. Login worked fine. Then I changed the user ID in the URL to a different number. Different user's data came back. No error. No log entry.

The dev had no idea. Cursor flagged nothing. The endpoint had authentication -- just no authorization at the resource level.

This is IDOR: Insecure Direct Object Reference (CWE-639). It is not exotic. It is the most boring, most common authorization failure in AI-generated APIs, and it almost never gets caught before production.

The Pattern AI Keeps Generating

Ask any AI editor to write a "get user profile" endpoint and you get something like this:

// CWE-639: any authenticated user can fetch any profile
app.get('/api/users/:id', authenticate, async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) return res.status(404).json({ error: 'Not found' });
  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

The authenticate middleware runs. The token is verified. The user is "logged in." But the endpoint returns any user's data for any valid ID. User 1 can fetch user 2's profile. User 2 can fetch user 100's orders. Every record in the database is one URL change away from exposure.

Why does AI generate this? Training data. Stack Overflow answers, GitHub tutorials, and "getting started" guides skip ownership checks in example code. The model learned from those examples. It reproduces the gap every time.

Why Standard Scanners Miss It

Authentication and authorization are different. Authentication: who are you? Authorization: what are you allowed to touch?

AI-generated code handles authentication correctly almost every time -- middleware, JWT validation, session checks, all generated properly. The gap is always at the resource level.

SAST tools look for SQL injection, XSS, missing crypto. They do not check business logic: "should this specific user be reading this specific record?" IDOR stays in the OWASP Top 10 year after year precisely because static analysis cannot catch it.

The Fix

Two parts: check ownership, return 403 when the check fails.

app.get('/api/users/:id', authenticate, async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) return res.status(404).json({ error: 'Not found' });

  // ownership check -- the line AI always omits
  if (user._id.toString() !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }

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

Return 403, not 404. Returning 404 hides whether the record exists -- but it also makes your API silently lie. For admin routes where any authenticated user should access any record, add a role check instead of removing the ownership check entirely.

Python/FastAPI version:

@app.get("/api/users/{user_id}")
async def get_user(user_id: int, current_user: User = Depends(get_current_user)):
    user = await User.get(user_id)
    if not user:
        raise HTTPException(status_code=404)
    if user.id != current_user.id and current_user.role != "admin":
        raise HTTPException(status_code=403, detail="Forbidden")
    return user
Enter fullscreen mode Exit fullscreen mode

Order matters. Fetch the resource first so you can compare IDs. Check ownership before returning.

Finding Existing IDOR Bugs in a Codebase

Grep for route handlers with dynamic parameters and no ownership check logic:

# Express -- routes that use :id params but never reference req.user
grep -rn "req.params.id" ./src --include="*.js" | grep -v "req.user"

# FastAPI -- route handlers with path params but no current_user comparison
grep -rn "async def get_" ./app --include="*.py" | grep -v "current_user.id"
Enter fullscreen mode Exit fullscreen mode

These greps are rough. Any match is a candidate for manual review, not a confirmed bug. But on a 20-file backend, this takes 10 seconds and surfaces the ones worth looking at.

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)