October 23, 2026
The Simple Login Screen Flaw That Exposed 132 Franchises
AI tools rarely write SQL injection now. Their apps still hand out secrets, database rows and unlimited requests. Here are five tests you…

By Junaid Ahmed Zama
6 min read
AI tools rarely write SQL injection now. Their apps still hand out secrets, database rows and unlimited requests. Here are five tests you can run on your own app this afternoon.
The dashboard had a login screen. That was the problem.
On 16 October, MyBroadband reported that an internal dashboard at Bootlegger, a South African coffee franchise, had been sending company data to anyone who opened its address. The login form worked. But the page that carried it also carried names and contact details linked to 132 locations, plus performance and financial figures, all readable in the page source. The address got out because employees posted photos of their laptop screens on Instagram. MyBroadband reported the app was vibe-coded with Claude. The company said it was a legacy version that hadn't been decommissioned, and took it offline the day it found out.
Three weeks earlier, UpGuard found 16,326 Supabase databases with tables anyone on the internet could read. Over half held personal data.
Neither leak needed a clever attack. Both came from an app handing over something it should have kept. That's the pattern this checklist is built on, and it's also why the security checks most people run first aren't the ones that matter most now.
What AI-built apps get wrong now
In July, Xint, the autonomous pentesting tool from security firm Theori, tested three AI-built apps: one built from a careful spec, one from a casual "just build this" prompt, and one legacy app that AI migrated to Laravel and React and then hardened. It found 434 exploitable issues.
The surprise was what was missing. SQL injection and cross-site scripting, the bugs at the top of most security checklists, barely appeared. The models have learned to parameterise queries and escape output.
Here's what filled the gap instead:
Of the 23 critical findings, 11 were hardcoded or default secrets and 6 were debug modes left on in a way that allowed remote code execution. It's one vendor's sample of three apps, so treat the counts as a direction, not a census. The direction matches the larger studies, though. The common thread is the app giving away data, keys or capacity, not breaking on malformed input.
The five checks below follow that order. Each one is a test you can run yourself, on an app you own. Don't point any of them at someone else's site.
Check 1: Read what the browser receives
A login screen is a curtain, not a lock. Anything your server sends to the browser before the user signs in, the user already has, whether or not the interface shows it. That's what happened at Bootlegger, and it's the most common way secrets leak.
The test takes five minutes. Open your app in a private window without signing in, then:
- Right-click and view the page source. Search it for names, emails and any data you'd expect to see only after login.
- Open DevTools, go to the Network tab and reload. Click each response and read what came back, especially JSON.
- In the Sources tab, search the JavaScript bundles for
sk_live,sk-,service_role,sb_secret_andeyJ(the start of most JSON web tokens).
Good looks like this: no user data before login, and the only key in the bundle is one designed to be public, such as a Supabase publishable key.
One cause deserves a sentence of its own. Frameworks bundle any environment variable with a public prefix into the browser code, such as NEXT_PUBLIC_ in Next.js or VITE_ in Vite. If an agent put a payment or AI-provider key behind that prefix to make an error go away, it's now public. Move the call to a server route, then rotate the key, because a key that was public once should be treated as stolen.
Run a secret scanner such as gitleaks over the repository history too. Deleting a key from the latest commit doesn't remove it from the history.
Check 2: Ask your database a question as a stranger
Supabase is a favourite of AI coding agents, and it works differently from a classic backend. The browser talks to the database directly, using a key that's meant to be public. What stops one user reading another's rows is Row Level Security (RLS), a set of rules inside Postgres that filter every query by who is asking.
Without it, the public key opens the table. Supabase's docs put it plainly: a table in an exposed schema without RLS "is readable and writable by any role with a grant on it." UpGuard's researchers pointed to one reason agents trip on this: tables created programmatically through the API, the way coding agents create them, don't have RLS switched on by default.
Test it the way an attacker would, with the key your frontend already ships:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: YOUR_PUBLISHABLE_KEY"curl "https://YOUR-PROJECT.supabase.co/rest/v1/profiles?select=*" \
-H "apikey: YOUR_PUBLISHABLE_KEY"Repeat for every table. If rows come back without a signed-in user, anyone can read them. Then open the Security Advisor in the Supabase dashboard, under Database. It flags "RLS disabled in public", "permissive RLS policy", "sensitive columns exposed" and views that bypass RLS.
The fix is to turn RLS on and write a policy for each table. For a table where users should see only their own rows:
alter table profiles enable row level security;
create policy "Users read their own profile"
on profiles for select
to authenticated
using ( (select auth.uid()) = user_id );alter table profiles enable row level security;
create policy "Users read their own profile"
on profiles for select
to authenticated
using ( (select auth.uid()) = user_id );RLS with no policies blocks everything, so expect parts of the app to break until each table has the policies it needs. Two more rules from the docs: the secret key (formerly service_role) skips RLS entirely and must never reach a browser, and views ignore RLS unless created with security_invoker = true.
This isn't only a Supabase problem. Firebase, PocketBase and any backend that lets the client query directly depend on the same kind of rules.
Check 3: Swap the ID
Insecure direct object reference, or IDOR, is a long name for a simple bug: the app checks that you're signed in but not that the record is yours. Change /api/orders/1042 to /api/orders/1043 and you're reading a stranger's order.
In Xint's tests, IDOR grew with the app, from 11% of flaws in the smaller apps to 28% in the larger one. That fits how agents work. Each new endpoint is written in isolation, and the ownership check that was in the first one doesn't always make it into the twentieth.
The test needs two accounts:
- Sign in as user A and create a record: an order, a profile, a file.
- In DevTools, copy the request that fetches it, including the ID.
- Sign in as user B in another browser and replay the request with B's session, unchanged.
If B gets A's data, the endpoint trusts the ID. Try the same with updates and deletes, which do more damage than reads. The fix is a server-side ownership check on every request, or an RLS policy that does it in the database. Hiding the button in the interface doesn't count.
Check 4: Hit it a hundred times
The biggest category in Xint's tests wasn't a data leak at all. It was endpoints with no limit on how often they can be called. Xint's report notes the impact "is not data theft" but "runaway server cost" or a server an attacker can knock over.
For an app built around an AI feature, this is a billing problem first. An endpoint that calls a paid model on every request turns anyone's loop of requests into a line on your invoice. Login and password-reset forms without limits invite password guessing and email spam sent under your domain.
Test it against your own app, starting on staging:
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://staging.yourapp.com/api/generate
done | sort | uniq -cfor i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://staging.yourapp.com/api/generate
done | sort | uniq -cGood looks like a run of 200 followed by 429 Too Many Requests. A hundred 200s means there's no limit.
Add limits per user and per IP, in middleware or at your host or CDN, starting with login, signup, password reset, file upload and anything that calls a paid API. Then set a hard monthly spending cap in your AI provider's dashboard, so a missed endpoint costs you a known amount.
Check 5: Force an error in production
Debug modes are the setting an agent turns on to get a stack trace while building and never turns off. In Xint's tests, six critical findings were debug modes that allowed remote code execution. Laravel's own docs say APP_DEBUG "should always be false" in production. Django's DEBUG and Flask's debugger carry the same warning.
The test: visit a URL that doesn't exist and send a request with a malformed body to an API route. If either response shows a stack trace, file paths, environment variables or SQL, debug is on. Set it off in the production environment, not just in a local .env file, and redeploy.
What your platform's scanner covers
Builder platforms have added security scanners, and they're worth running. They also say plainly what they don't do. Lovable's own documentation says its automatic pre-publish scan reviews database rules and dependencies, not your application code. Its deeper scan covers code but runs only when you ask for it. Both are static: they read the code and configuration, and don't test the running app. Lovable recommends a professional review for apps with sensitive data.
So the scanner catches the first half of Check 2 and some of Check 1. Checks 3, 4 and 5 are about how the running app behaves, which a static scan can't see.
The habit that matters
None of these five checks needs a security background. They need the habit of looking at your app from the outside, as a stranger who has the URL and a browser. Agents build from the inside, one feature at a time, and they don't take that view unless you ask for it.
Run all five before the first real user signs up. Then run checks 2 and 3 again every time an agent adds a table or an endpoint, because that's when the gaps come back.
Which of these five would your current project fail first?