Here is the loop most apps are built in now. You describe what you want. The model writes it. It runs. You ship it. The loop is fast and it works, and that is why it is everywhere.
There is a step missing from it. Nothing in the loop looks at the code for security. Not the model, because it wrote the code and cannot see what it missed. Not the platform: Lovable's own blog says its Security Scan "catches many issues, but sometimes incorrect database security setups slip through" (Lovable's blog). And not you, because you are not reading the code, and you were never going to.
The 2026 measurements of AI-built apps are consistent enough to take seriously. A June 2026 scan of 62 Lovable apps found 63% carried critical or high severity vulnerabilities, with 10 findings per app on average (DEV.to, Jun 2026). VibeSafe studied 400 public repositories built with Lovable, Bolt and v0: 59% had at least one critical issue, and 18.5% had committed a .env file. Escape DAST analysed 5,600 vibe-coded apps and found 2,038 critical vulnerabilities and 400+ exposed credentials (via Lifehacker, Aug 2026). symbioticsec.ai found that 98% of apps behind a public Supabase URL had at least one exploitable flaw. And vibe-eval measured 37.6% more critical vulnerabilities after just five AI build iterations, so keeping the prompts coming makes the gap wider.
The same five mistakes, every time
The AI writes the happy path and never checks the answer.
export async function getCurrentUser() {
const { data: { user } } = await supabase.auth.getUser();
const userId = user.id;
const email = user.email;
return { userId, email };
}
getUser() returns { data, error }. If nobody is signed in, user is null and user.id throws. If the request fails, the code carries on with an undefined user. The check is three lines:
const { data, error } = await supabase.auth.getUser();
if (error || !data?.user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
}
const user = data.user;
The server reads a session it never verified.
export async function readSession() {
const { data: { session } } = await supabase.auth.getSession();
return session.user.id;
}
getSession() reads the session cookie but does not check the signature with the authentication server. On the client, that is fine, because the cookie only decides what to render. On the server, it means the visitor answers the question "who is this". A hand-edited cookie is accepted as valid. On the server, ask the auth server instead:
const { data: { user }, error } = await supabase.auth.getUser();
if (error || !user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
}
A permission check that trusts the browser.
export function isAdmin() {
return localStorage.getItem('role') === 'admin';
}
Browser storage is entirely under the control of the person using the browser. Change the value in the developer tools and the admin panel opens. Permissions belong on the server, and in the database rules, not in a value the visitor can edit.
A token check that accepts an unsigned token.
const SECRET = process.env.JWT_SECRET || 'change-me-in-production';
export function verifyToken(token) {
return jwt.verify(token, SECRET, { algorithms: ['none', 'HS256'] });
}
Two problems. 'none' means a token with no signature at all is accepted, and anyone can write one by hand claiming to be any user, including an admin. The fallback means that if the environment variable is ever missing in production, every token gets signed with a value written in plain sight in the source. The fix is to fail loudly and pin the algorithm:
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET is not set');
const payload = jwt.verify(token, secret, { algorithms: ['HS256'] });
A table with no rule on it.
create table public.bookings (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.profiles (id),
total numeric(10, 2) not null,
shipping_address text,
created_at timestamptz default now()
);
Nothing after that statement turns on Row Level Security. The full fix, including the policies for each operation, is in the RLS guide linked below.
What this means for you
None of this says the code is bad. AI-generated code is often good code. The gap is a missing verification step, and the fix is to add it once, not to rebuild anything. The mistakes above are all small edits to small places.
It is worth being precise about what the numbers do and do not say. They describe a population of apps that were scanned, and the way those apps were built is not random. They are apps where a person asked a model to produce a working product quickly, and then shipped it. That population describes most AI-built apps, which is why the same handful of mistakes keeps showing up in it. It is not a claim that every AI-built app is broken, and it is not a claim that any particular app of yours is broken. It is a claim that nobody has looked yet.
What changes when somebody does look is that the list of unknowns gets shorter. The first pass usually finds a handful of specific things, and most of them are one or two lines to change. The second pass finds less. The third finds almost nothing, and you can stop.
What to do next
Run the free scanner. It reads your project folder in your browser and checks the six things that matter most in an AI-built project: environment files committed to git, hard-coded secrets, row-level security on your tables, token verification, cross-origin rules, and leftover debug routes. Nothing is uploaded, there is no account, and there is no email address. You get a ship-readiness score out of 100 and a paste-ready fix for every finding. It is not proof that your app is secure. It is a record of what was checked and what was not, which is the step the build loop skipped.
The scanner that finds this is open source: github.com/rafikulislammollaq-dev/vibeaudit
It also runs in the browser, where your project is read on your own machine and never uploaded: rafikulislammollaq-dev.github.io/vibeaudit/scanner
If you would rather have a person read the findings and explain them, that is a $199 review: rafikulislammollaq-dev.github.io/vibeaudit
Top comments (0)