The bugs that get AI-built apps breached are almost never crashes. They are authorization holes: the app hands back data it should have denied, returns a normal response, and looks completely healthy. Five show up again and again, and every one passes tests and code review. Here is what each looks like, why the model cannot see it, and why each is a reachability property you can prove rather than an opinion you have to trust.
Talk to anyone reviewing AI-built apps and you hear the same list. Change a number in a URL and read someone else's account. Find an admin page with no login. Send one extra field in a form and become an admin. None of it throws an error. The app works, users sign up, payments clear, and the hole stays open until the day someone tries the request your tests never sent. These are not exotic bugs. They are the boring, well-documented failures of access control, and AI produces them at scale because the code that has the bug looks exactly like the code that does not.
The classic. A handler takes an id from the request and looks the row up, but never checks that the row belongs to the caller. Change /orders/1001 to /orders/1002 and you get someone else's order. Change the id in a profile or invoice endpoint and you walk the whole table. The fetch-by-id code looks finished, which is exactly why it ships.
One endpoint checks that you are an admin; its sibling, added later, forgot to. The delete route is guarded, the bulk-delete route is not. Because the guarded and unguarded versions sit side by side and both return 200 in normal use, nothing looks off. An attacker just calls the one that skipped the check.
A self-service update binds the whole request body onto the record. You send the fields the form shows, plus one it does not: "role": "admin", or "account_id": 7. The model wrote a tidy "update the user from the body" and never listed which fields the client is allowed to own. The extra key rides straight through.
invariant: role_is_server_controlled (user.role stays "member" across a self-update) REFUTED 1. <init> user.role = "member" 2. PATCH /api/profile body = { "name": "x", "role": "admin" } 3. bind body -> user user.role = "admin" // client set a field it should not own
You hardened /api/v2 with the ownership and rate checks. But /api/v1 is still deployed, still routes to the old handler, and skips every control v2 enforces. The new code is correct; the old code never went away. Attackers read your changelog and go straight for the version you stopped thinking about.
A multi-tenant app is one WHERE tenant_id = ? away from a data breach. Drop that clause in one query, or forget it on a new report endpoint, and one customer reads another customer's rows. The query returns clean JSON, the dashboard renders, and nobody notices until the wrong logo shows up in someone's export.
Two reasons, and they compound. First, the buggy version is the probable version: most code the model learned from shows the fetch, the update, the query, not the guard wrapped around it, so "correct-looking code without the check" is exactly what it tends to generate. Second, the model cannot run the app. It never sends the request with the swapped id or the extra field, so it has no way to notice the response came back when it should not have. Ask it "is this secure?" and you get the most probable answer, which is a reassuring yes. That yes was never checked against anything.
Notice that every bug above reduces to a single sentence about a state the app should never reach: the viewer is not the owner, the caller lacks the role, a client-owned field changed a server field, an endpoint skipped a guard, a row crossed a tenant line. That is the whole trick. Once the rule is written as an invariant, catching the bug stops being a matter of opinion and becomes a search: explore the reachable states of the code and either return a concrete path that violates the invariant or prove none exists within the bound.
That is what RedMirror does. It runs inside your coding agent, on your machine. The model proposes where an authorization rule should hold; a compiled kernel lifts the code to a state machine and searches for a way to break it, handing back a replayable path like the one above or a bounded proof there is no such path. A finding is something you can rerun, not a paragraph you have to believe. And because these bugs are invisible by nature, that difference is the whole game: you cannot eyeball your way to "the tenant filter is present on every query," but you can prove it.
Because they are not crashes. A broken authorization check returns a normal, successful response with data that should have been denied. Your tests cover the happy path, the app runs fine, and the hole only opens when someone sends the request your tests never sent. That is why these bugs sit in production for months.
Broken object-level authorization, also called IDOR. You change an id in the request, like /orders/1001 to /orders/1002, and the server returns another user's record because it looked the row up by id but never checked that the row belongs to the caller. It is common because the happy-path code that fetches by id looks complete and correct.
The model generates the most probable code, and code without an ownership check is very probable because most examples it learned from show the fetch, not the guard. It also cannot see the deployed app or run the request, so it has no way to notice that a check is missing. Asking it whether the code is secure returns a confident yes that was never verified.
State each rule as an invariant, like the viewer equals the record owner, then search the reachable states of the code for a way to violate it. The result is either a concrete, replayable path into the bad state or a bounded proof that none exists. That is what RedMirror does: the model proposes where to look and a compiled kernel proves or refutes the path.
RedMirror runs inside your coding agent and proves reachable authorization and logic bugs with a replayable path, or a bounded proof there is none. Anything that reads a record by an id from the request is a good place to start.
Get RedMirror How to find IDOR and logic bugs