Guide
What AI coding tools get wrong about authorization
AI coding tools are good at producing a feature that works. Authorization is different: it is a rule about who may do what, and it is usually invisible in the feature description. "Add an endpoint to update an order" does not say "only the person who owns the order". The model builds what you asked for. The rule you did not state is the one that goes missing.
This is not a theoretical worry. In February 2026, The Register reported on an app showcased by Lovable that exposed 18,697 user records; the researcher found 16 vulnerabilities, 6 of them critical, including inverted authentication logic that let unauthenticated users through (Lovable said the app included code it did not generate and that the database was not hosted by it). In June 2026, Symbiotic Security scanned 1,072 Supabase-backed apps built with AI tools and found 172 that allowed unauthenticated data deletion. That scan is vendor research, and most of its headline "98% had an issue" is lesser items such as missing headers, so the 172 is the number worth reading.
Here are the four authorization mistakes we check first.
1. The route that checks nothing
app.patch('/api/orders/:id', async (req, res) => {
await db.orders.update(req.params.id, req.body)
res.sendStatus(204)
})
There is no session check, so anyone can call it. There is no ownership check, so any logged-in user can change anyone's order. And req.body goes straight to the database.
A safer shape:
app.patch('/api/orders/:id', requireUser, async (req, res) => {
const order = await db.orders.findOne({ id: req.params.id, userId: req.user.id })
if (!order) return res.sendStatus(404)
const { shippingAddress } = req.body // allow-list of fields
await db.orders.update(order.id, { shippingAddress })
res.sendStatus(204)
})
Three ideas: require a user, scope the lookup to that user, and update only fields you name. Returning 404 rather than 403 for someone else's order avoids confirming that the id exists.
2. Mass assignment
If a profile update accepts the whole body, a user can send {"role": "admin"} or {"credits": 9999}. Generated code often does this because it is the shortest path. Check every create and update handler for a field allow-list. In TypeScript projects, parse input with a schema (for example, Zod) that lists the permitted keys and strips the rest.
3. Authorization only in the UI
A common pattern: the admin button is hidden for ordinary users, and the underlying API route is open. Hiding a button is user experience, not security. Every rule must be enforced where the data is read or written. A quick test: log in as an ordinary user, copy a request an admin makes (browser dev tools, "copy as curl"), replay it with your own session. If it succeeds, the rule lived only in the interface.
4. Database rules that were never written
With Supabase, the browser talks to the database directly using a public key, and row-level security (RLS) decides what that key can see. The generated schema often creates the tables and omits the policies.
alter table public.orders enable row level security;
create policy "owners read own orders"
on public.orders for select
using (auth.uid() = user_id);
create policy "owners update own orders"
on public.orders for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
Note the with check clause on update. Without it, a user can update their own row and change user_id to someone else's. Also remember that the service-role key bypasses RLS entirely; it must never reach the client bundle.
Inverted logic, as in the reported case, is the failure no scanner flags easily: the check is present but allows the wrong people. For example:
if (user.isAdmin) return res.sendStatus(403) // should be !user.isAdmin
Only a test with a real second account catches it.
A practical test routine
You do not need special tools for a first pass.
- List every route and write beside it who may call it. Public, any logged-in user, owner only, admin only. A route with no answer has no authorization.
- Create two ordinary accounts, A and B. As A, create data. As B, try to read, change and delete it by id. Every attempt should fail.
- Test logged out. Replay requests with no session or token.
- Test role escalation. Send extra fields (
role,isAdmin,price) in create and update calls. - Check the database layer. For each table, confirm RLS is enabled and each operation (select, insert, update, delete) has a policy.
Write these as automated tests once, so the next AI-generated change cannot silently undo them.
Tell the tool the rule
AI tools respond well to explicit rules. Instead of "add an endpoint to update an order", write: "Only the authenticated owner may update their own order; only shippingAddress is editable; return 404 for other users' orders; add tests with two accounts." You get the rule in the code and the tests that protect it. You still need to verify, because a model can state a rule and implement it wrongly.
Where a human reviewer helps
Authorization is business logic, so scanners see little of it. A person reading the routes with the question "who may call this?" finds in an hour what a pattern-matching tool will not.
Want a senior engineer to run this for you?
The Launch Security Check covers secrets, authentication, authorization, database rules, API exposure and dependencies for one app, from US$790 fixed. Every finding is confirmed by a senior engineer and comes with a fix. It is a review of code and configuration with the access you grant, not a penetration test or a certification.
Sources
- The Register, 27 February 2026, report on a Lovable-showcased app exposing 18,697 user records (theregister.com)
- Symbiotic Security, "We scanned 1,072 vibe-coded apps", 2 June 2026 (symbioticsec.ai/blog)
- Code samples: written by Proofmerge for this guide.