{"slug": "why-cursor-writes-idor-into-your-api-routes-cwe-639", "title": "Why Cursor Writes IDOR Into Your API Routes (CWE-639)", "summary": "A developer discovered that Cursor, an AI code editor, generated an API endpoint vulnerable to Insecure Direct Object Reference (IDOR, CWE-639). The endpoint authenticated the user but failed to verify ownership of the requested invoice, allowing any logged-in user to access another user's private data by changing the ID in the URL. The developer fixed the issue by scoping the database query to the authenticated user's ID.", "body_md": "I asked Cursor to build an endpoint that returns an invoice by ID. It gave me clean code. Auth middleware on the route, a database lookup, a JSON response. It ran on the first try.\n\nThen I logged in as a different test user and changed the number at the end of the URL. Invoice #1001 belonged to someone else. I got the whole thing back: amount, line items, billing address. No error, no warning. Just another user's private data on my screen.\n\nThat is IDOR, an Insecure Direct Object Reference, and it is one of the most common holes I find in AI-generated APIs. The frustrating part is that the code looks secure. It even has an auth check. It just checks the wrong thing.\n\nThe endpoint below is broken because it confirms you are logged in but never confirms the invoice is yours. `findById`\n\ntakes the ID straight from the URL and returns whatever it finds.\n\n```\n// CWE-639: authenticated, but no ownership check\napp.get('/api/invoices/:id', authenticate, async (req, res) => {\n  const invoice = await Invoice.findById(req.params.id);\n  res.json(invoice);\n});\n```\n\nThe `authenticate`\n\nmiddleware does its job. It proves the request comes from a real, logged-in user. What it does not prove is that this particular user has any right to invoice `:id`\n\n. Change the ID, get someone else's record. Increment it in a loop and you can walk the entire table.\n\nAI editors confuse authentication with authorization because almost every tutorial they trained on does the same thing. Authentication is \"who are you.\" Authorization is \"are you allowed to touch this specific object.\" Most example code stops at the first one.\n\nWhen a tutorial demonstrates a \"get resource by ID\" route, the shortest version that runs is `Model.findById(req.params.id)`\n\n. That snippet appears thousands of times in the training data, almost always without an ownership filter, because the tutorial's goal is to show routing and database access, not object-level access control. The model reproduces the pattern it saw most. It even adds the `authenticate`\n\nmiddleware, because that is a visible, nameable step. Ownership is invisible. It lives in the shape of the query, and there is no keyword for the AI to copy.\n\nScope the lookup to the authenticated user so the database can only ever return records that belong to them. Never fetch by a raw ID from the request and trust it.\n\n```\n// Fixed: the query itself enforces ownership\napp.get('/api/invoices/:id', authenticate, async (req, res) => {\n  const invoice = await Invoice.findOne({\n    _id: req.params.id,\n    userId: req.user.id,\n  });\n  if (!invoice) return res.status(404).json({ error: 'Not found' });\n  res.json(invoice);\n});\n```\n\nThe change is small but the logic is different. The query now says \"find this invoice and confirm it is mine\" in a single step. If the ID belongs to another user, the database returns nothing and the caller gets a 404, which also avoids confirming that the record exists at all.\n\nThe same rule holds in Python. Filter by owner in the query, do not fetch first and check later.\n\n```\n# Fixed (FastAPI + SQLAlchemy)\n@app.get(\"/api/invoices/{invoice_id}\")\ndef get_invoice(invoice_id: int, user=Depends(current_user), db=Depends(get_db)):\n    invoice = (\n        db.query(Invoice)\n        .filter(Invoice.id == invoice_id, Invoice.user_id == user.id)\n        .first()\n    )\n    if invoice is None:\n        raise HTTPException(status_code=404, detail=\"Not found\")\n    return invoice\n```\n\nDo this on every route that reads or writes a specific object: invoices, orders, messages, uploaded files, profile settings. Anywhere an ID comes from the client, the query has to prove ownership, not just existence.\n\n**Q: Isn't login enough to protect an API route?**\n\nA: No. Login proves who the user is. It says nothing about whether that user owns the specific record they are requesting. You need an ownership check on every object-level route in addition to authentication.\n\n**Q: Do UUIDs instead of sequential IDs fix IDOR?**\n\nA: No. Random UUIDs make IDs harder to guess, but that is obscurity, not access control. If an ID leaks in a URL, log, or referrer header, the record is still exposed. Always enforce ownership in the query regardless of ID format.\n\n**Q: How do I find IDOR in code I already shipped?**\n\nA: Look for any route that reads an ID from the request and calls `findById`\n\n, `get`\n\n, or a raw query without filtering by the current user. Every one of those is a candidate. Testing is simple: log in as user A, request user B's IDs, and see what comes back.\n\nI've been running [SafeWeave](https://tinyurl.com/237e23ht) for this. It hooks into Cursor and Claude Code as an MCP server, and its posture and SAST scanners flag routes that fetch an object by a request-supplied ID without an ownership filter, before I move on to the next file. Even a careful code-review checklist that asks \"does this query prove ownership, not just existence?\" will catch most of what's in this post. The important thing is catching it early, whatever tool you use.", "url": "https://wpnews.pro/news/why-cursor-writes-idor-into-your-api-routes-cwe-639", "canonical_source": "https://dev.to/c_k_fb750e731394/why-cursor-writes-idor-into-your-api-routes-cwe-639-imi", "published_at": "2026-07-28 18:31:11+00:00", "updated_at": "2026-07-28 18:35:33.611426+00:00", "lang": "en", "topics": ["ai-tools", "ai-safety", "developer-tools"], "entities": ["Cursor"], "alternates": {"html": "https://wpnews.pro/news/why-cursor-writes-idor-into-your-api-routes-cwe-639", "markdown": "https://wpnews.pro/news/why-cursor-writes-idor-into-your-api-routes-cwe-639.md", "text": "https://wpnews.pro/news/why-cursor-writes-idor-into-your-api-routes-cwe-639.txt", "jsonld": "https://wpnews.pro/news/why-cursor-writes-idor-into-your-api-routes-cwe-639.jsonld"}}