All posts

Field guide

Broken access control: the bug AI puts in almost every app

AI tools are good at building a login and bad at protecting what is behind it. The result is broken access control: a user is correctly signed in, then reaches data that is not theirs by changing an id in a request. It is one of the most common and most exploitable issues in AI-built apps. Here is why, and how to catch it.

Last updated

A login is the part of security that is easy to see, so AI tools build it well. What they routinely miss is the quieter half: authorization, the per-request check that a signed-in user is actually allowed to touch the specific thing they are asking for. When that check is missing, you have broken access control, and it is both common and easy to exploit, because it needs nothing more than changing a number in a request.

What it looks like

The endpoint takes an id from the request, looks up the record, and returns it, without confirming the record belongs to the caller. Every normal test passes, because a real user requests their own id. Change the id and the same route hands back someone else's data.

// the vulnerable pattern: fetch by id, no ownership check
app.get("/api/orders/:id", requireAuth, async (req, res) => {
  const order = await db.order.findUnique({
    where: { id: req.params.id },
  });
  res.json(order);            // any logged-in user can read any order
});

Notice the login is present, requireAuth runs, so this is not an authentication failure. The missing piece is the check that the order belongs to req.user. This specific shape is often called IDOR, an insecure direct object reference.

Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.

See a live scan →

Why it is so common in AI-built apps

The ownership check is not required for the feature to work, so an assistant optimizing for running code leaves it out. The endpoint demos perfectly, the gap is invisible in a functional test, and it only appears when someone walks through ids on purpose. That combination, easy to write without it and invisible until attacked, is why it survives to production so often.

The fix: scope every operation to the user

Make the server confirm ownership on every read and write. Either filter the query by the authenticated user's id, or fetch and then verify before returning or changing anything.

app.get("/api/orders/:id", requireAuth, async (req, res) => {
  const order = await db.order.findUnique({
    where: { id: req.params.id },
  });
  if (!order || order.userId !== req.user.id) {
    return res.sendStatus(404);
  }
  res.json(order);
});

Do it in server code, never only in the UI, since anyone can call the route directly. The full walkthrough is in the fix for a missing authorization check, and this is question two of a five-part review pass worth running on every diff.

Frequently asked questions

Isn't a login enough to protect my data?

A login handles authentication, proving who someone is. Protecting data also needs authorization, deciding what that known person is allowed to do. Broken access control is what happens when the second step is missing: a user is correctly logged in, and then the app lets them reach records that belong to other users, because nothing checks ownership on each request. The login is real; the per-action check is not.

Why do AI tools skip the authorization check so often?

Because the feature works without it. The endpoint returns the right data to the right user in every normal test, and the ownership check is not needed to make it function. The assistant optimizes for working code, so the check that only matters when a different user changes an id is exactly the kind of step it leaves out. That is why this is one of the most common serious issues we find.

Where does broken access control usually show up?

In any endpoint that reads or changes a record by an id from the request: viewing an invoice, editing a profile, downloading a file, deleting an item. If the code looks the record up by that id and returns it without confirming it belongs to the caller, changing the id reaches someone else's data. This specific case is often called IDOR, or insecure direct object reference.

How do I fix it across the app?

Make every data operation scope to the authenticated user on the server. Either filter the query by the user's id, or fetch and then verify ownership before acting, and do it in the server code, not the UI, since a UI check is bypassed by calling the route directly. Build the habit of asking, for each endpoint, does this confirm the record belongs to the current user, and treat a missing answer as a bug.

Find the routes that trust the id

An endpoint that looks up a record by a request id without an ownership check has a recognizable shape, and a scan flags it with the file and line, so you can add the check before someone changes the id. Run a free scan and see which of your routes serve anyone's data.

Prbl scans your live app or your codebase for exactly the kinds of issues above.

Broken Access Control: The Bug AI Puts in Almost Every App: Prbl