← All news
security5 min read

Insecure direct object references in AI-generated API endpoints

By Seaworthy · 25 June 2026

Close-up of a blue wooden door with a metal handle, suggesting restricted access and locked entry

Photo by Jan Tinneberg on Unsplash

What AI assistants get wrong about CRUD

Ask an AI assistant to scaffold a REST API and it will produce something that looks correct. Authentication middleware, route handlers, database queries, a response schema. It all compiles. It all runs. And in a good chunk of cases, any authenticated user can read or modify any other user's data.

This is the insecure direct object reference problem, and it is one of the most common vulnerabilities in AI-generated backends. Not because the model doesn't know about it. It does. It's because the model's default is to satisfy the request, and the request was "give me a GET /invoices/:id endpoint", not "give me a GET /invoices/:id endpoint that correctly enforces ownership".

The pattern that keeps appearing

Here is the kind of code that gets generated:

// GET /invoices/:id
app.get('/invoices/:id', authenticate, async (req, res) => {
  const invoice = await db.invoice.findUnique({
    where: { id: req.params.id },
  });

  if (!invoice) {
    return res.status(404).json({ error: 'Not found' });
  }

  return res.json(invoice);
});

The authenticate middleware confirms the request carries a valid token. The handler fetches a record. The bug: there is no check that invoice.userId matches the authenticated user's ID. User A can request /invoices/42, get a 200, and read User B's invoice. Change the method to PATCH or DELETE and the damage gets worse.

This isn't a subtle edge case. It's a straightforward read of someone else's data, no exploit required, just a different ID in the URL.

Why AI models miss it

The model is optimising for a working, readable implementation. Ownership checks are invisible when everything is working normally because in a standard test flow you are always the same user. The check only matters when someone deliberately tries another user's ID, and that isn't a scenario the model assumes unless you tell it to.

There's also a signal problem. A lot of training data shows authentication middleware being added and then the business logic proceeding without an ownership assertion. This is normal in internal tools where every user is an admin, or in single-tenant systems. The model generalises from that.

The fix

Two lines. Fetch the record, then check ownership before returning it.

// GET /invoices/:id
app.get('/invoices/:id', authenticate, async (req, res) => {
  const invoice = await db.invoice.findUnique({
    where: { id: req.params.id },
  });

  if (!invoice) {
    return res.status(404).json({ error: 'Not found' });
  }

  // Ownership check — return 404, not 403, to avoid leaking existence
  if (invoice.userId !== req.user.id) {
    return res.status(404).json({ error: 'Not found' });
  }

  return res.json(invoice);
});

Return 404 rather than 403. A 403 tells an attacker the record exists but they can't see it, which is useful information. A 404 gives them nothing.

Alternatively, move the ownership check into the query itself:

const invoice = await db.invoice.findUnique({
  where: {
    id: req.params.id,
    userId: req.user.id,  // ownership enforced at query time
  },
});

This is cleaner. The database returns null if the record doesn't belong to the requesting user, so you get the correct 404 behaviour for free. It also prevents a class of time-of-check to time-of-use bugs where the ownership could theoretically change between the fetch and the assertion.

Where to look in your own code

The vulnerable pattern is any route that accepts a resource ID in the URL or request body, queries a table with that ID, and does not constrain the query to the authenticated user's records. It appears in:

  • GET endpoints that retrieve a single resource by ID
  • PATCH and PUT endpoints that modify a resource
  • DELETE endpoints
  • Any endpoint that accepts an ID as a query parameter and uses it to look up related records

The mutation endpoints are the ones that hurt most. Reading another user's invoice is a data exposure. Deleting it, or changing the amount, is active damage.

Auditing generated code

When you review AI-generated API code, check every route handler that touches the database. Ask: could a request with a valid token but a different user's resource ID succeed? If the answer is yes, the ownership check is missing.

A concrete test: write a test that authenticates as User A, creates a resource, then authenticates as User B and tries to read, modify, or delete that resource by ID. It should get a 404. If it gets a 200, you have an IDOR.

This is not a theoretical risk. IDOR vulnerabilities consistently appear in the OWASP Top 10 for a reason: they are easy to introduce, invisible in normal use, and immediately exploitable by anyone who bothers to look.

Seaworthy's static analysis checks for this pattern specifically, flagging route handlers where a resource ID from the request is used in a database query without a corresponding ownership constraint on the authenticated user.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode