The authorisation bug AI coding tools write by default
Kris · Fulcrum KD · updated 2026-08-23
AI coding assistants are good at authentication and bad at authorisation. They will reliably write a route that confirms you are logged in, then hand back a record without ever checking whether that record is yours. Your tests pass, because tests are written from the happy path, and the happy path is always your own data.
What the bug actually looks like
The shape is always the same. A route is protected, in the sense that an unauthenticated request bounces. Inside the route, the handler takes an identifier straight from the request and looks it up. Nothing between those two steps asks whether the person holding the token is entitled to that particular row.
Change the number in the URL and you get someone else's record. That is the whole exploit. There is no clever payload and no injection, which is precisely why it survives: it does not look like an attack in the logs, it looks like a customer loading a page.
Why the assistant writes it and why you do not notice
It is a prompting artefact. You asked for a route that returns a user's profile. You did not ask for a route that returns a user's profile only when the requester is that user or an administrator, because that qualifier lives in your head as an obvious background assumption. The model writes what was specified, and the thing that was not specified is the thing that matters.
It survives review for a related reason. The code reads correctly. There is an authentication middleware at the top, so the route is visibly protected, and the eye stops there. Distinguishing authentication from authorisation is a habit that comes from having been burned, and it is exactly the habit an assistant does not have.
And it survives tests because tests are written by the same process that wrote the code, from the same happy path. A test that fetches your own record passes whether or not the ownership check exists. Only a test that fetches someone else's record with your token can tell the difference, and nothing prompts anyone to write that one.
What we found in a 49-route sweep
We ran an authorisation sweep across 49 routes of a production API we build and operate. The worst finding was not a missing check. It was an inverted one.
A redaction condition, meant to strip sensitive fields from a user record before returning it to anyone who was not the owner, had its condition the wrong way round. The effect was that the redaction applied to the owner and not to everyone else. The full record went out to any authenticated caller who asked for it, which is an account takeover path rather than a privacy nuisance.
That bug had been sitting in a route that was authenticated, reviewed, and covered by passing tests. It is the reason we now treat every route as unaudited until someone has specifically asked, of that route, who is allowed to read this and who is allowed to write it.
How to audit your own app in an afternoon
You do not need tooling for the first pass. You need a list and a second account.
- List every route that reads or writes something belonging to a user. Not every route, just the ones touching owned data. In most small apps this is between twenty and sixty.
- For each one, write down in plain words who is allowed to do it. If you cannot state the rule in a sentence, the code cannot be enforcing it.
- Create a second account with no relationship to the first. This is the whole test rig.
- With the second account's token, request the first account's records by identifier. Anything that returns data is a finding. Anything that accepts a write is a serious finding.
- Check that ownership is enforced in the query itself, not after it. A check that fetches the row and then decides whether to return it will leak through timing, error differences, and every future refactor that forgets the second step.
- Read every redaction and permission condition in both directions. Ask what happens when the condition is true and when it is false, out loud. That is how the inverted one above should have been caught.
- Check the write paths for mass assignment. A route that takes an object and updates a record with it will happily accept fields the user should never control, such as role or account status.
What to do when you find one
Fix the query first, so the vulnerability is closed, then work out the blast radius. Your access logs will tell you whether the identifiers being requested ever belonged to someone other than the requester. In most cases the answer is no and you can breathe out.
Then write the test you did not have: the second account requesting the first account's data, asserting a refusal. One test per class of route is enough to stop the bug coming back the next time an assistant regenerates that file, and it will.
If sensitive fields were exposed, rotate anything that was in the payload and treat it as disclosed rather than probably fine. That judgement is cheap to make correctly and expensive to make optimistically.
Common questions
- Does this only affect AI-written code?
- No. Broken object-level authorisation predates AI assistants by decades and is one of the most common serious web vulnerabilities there is. What changed is the rate. An assistant will produce the vulnerable pattern quickly, consistently, and across many routes at once, so a small app can acquire a large number of instances in a single afternoon of building.
- Will a scanner find it?
- Usually not on its own. Automated scanners are good at injection and misconfiguration and poor at authorisation, because the tool has no way to know that record 41 should belong to a different person than record 42. Finding it reliably needs someone who understands what the data means.
- How long does a proper audit take?
- The first pass described above is an afternoon for a typical small application. A thorough sweep with the write paths, the redaction conditions and a regression test per route class is a few days. The 49-route sweep referenced here took the larger end of that.
- We are pre-launch. Does this matter yet?
- It matters most in the window right after launch, when you have real user data and no monitoring. Auditing before that window is dramatically cheaper than disclosing during it.
// from fulcrum
Get a free code health check// read next
- You built an app with AI and people use it. Now what?
Four checks for founders whose Cursor, Claude or Lovable app now has real users: secrets, backups, tests, and when to bring in an engineer.