FOR PEOPLE SHIPPING LOVABLE APPS

What Lovable apps get wrong

In round 2026-09 of the Vibe Security Index we scanned 50 public repositories carrying Lovable's scaffold marker with all 212 rules. These are the security findings that repeated, why this particular workflow produces them, and the prompt change that prevents each one.

Read this first

  • This is not a claim that Lovable's product is insecure. We measured public repositories carrying its scaffold marker (lovable-tagger in package.json), edited by humans for an unknown period afterwards. The marker proves the project started from the scaffold. It does not prove any particular line was generated rather than written, and attributing a finding to the builder is not something this design supports.
  • 50 repositories, drawn from a universe of 76,672. Every rate on this page is stated with the count behind it, because a percentage over 50 is a different kind of object than a percentage over 76,672: the finest distinction 50 repositories can draw is one repository. Differences of a few points here are noise.
  • No repository is ever named. Not here, not on the Index, not in the working data. Publishing the list would mean pointing at live vulnerabilities in other people's projects, so the results are aggregate only and the per-repository rows are anonymised by design.
  • Static analysis has false positives, including ours. A finding is something a person should look at, not a proven breach. Where a finding on this list is partly our own noise, it says so directly underneath.

Round 2026-09, generated September 3, 2026. Full method, sampling frame and limitations: Index methodology.

The shape of the sample

70%

had at least one critical finding in their own code

35 of 50

100%

had at least one high or worse

50 of 50

40

median findings per project, application code only

across 50 projects

126

median scanned files per project

size is a confound on every rate above

Read the second number carefully. The high-or-worse rate is 100% because Missing Content Security Policy (CSP) fired on all 50 projects, and that check is rated high. It is a response-header setting on the host, not a flaw in anyone's code. The critical rate to its left is the number that carries information about the code.

Why the loudest findings are not on this page

Sorted by raw occurrence count, the top of this builder's results is code quality, not security. Magic Numbers in Code (1,071 occurrences across 50 of 50 projects), Console.log Left in Production Code (1,037 occurrences across 40 of 50 projects) and N+1 Query Pattern Detected (286 occurrences across 22 of 50 projects) are all info severity, a call this codebase made deliberately and has not changed. They are worth cleaning up. They are not what someone shipping an app to real users is asking about, and a security page that led with them would be counting lines instead of answering the question.

So the list below is filtered by severity and then ordered by severity, with how many projects a finding appeared in as the tiebreaker. Occurrence count never decides the order. One finding here fired 572 times inside twelve projects, and letting that sort the page would have put an information leak above a leaked key. What leads instead is Hardcoded API Key or Secret, the list's only critical-severity finding, in 28 of 50 projects.

Security findings that repeat, application code only

From round 2026-09. n = 50 repositories. “Projects” is how many of them contained the finding at least once; occurrences is the total across all of them.

FindingSeverityProjectsOccurrences
Hardcoded API Key or SecretVC001critical56%28 of 50176
Missing Content Security Policy (CSP)VC020high100%50 of 50108
API Route Missing AuthenticationVC003high38%19 of 50275
Missing Security Meta Tags / HeadersVC027medium100%50 of 50217
Clickjacking — Missing X-Frame-OptionsVC056medium100%50 of 50122
Missing Subresource Integrity (SRI)VC084medium46%23 of 5029
Stack Traces Exposed in API ResponsesVC037medium38%19 of 50749

This is the round's top-rules list with info and low severity removed. It is not the complete finding list for any project, and a check that never made this builder's top ten can still be the one that matters in yours.

Each one, and what stops it coming back

Fixing the finding fixes one line. Changing the instruction that produced it fixes every line the next prompt writes, which is the only version of this that scales when the code is arriving faster than it can be read.

criticalVC001 · 28 of 50 projects · 176 occurrences

Keys that end up in the browser bundle

Hardcoded API Key or Secret

Why this workflow produces it

A Lovable project is a Vite single-page app. There is no server in it, so there is nowhere in the generated project for a secret to live — every string in the repository is compiled into JavaScript that anyone can download and read.

The moment you add a database — and Supabase is the usual answer — the project gains a client config with a key written inline. The codebase now demonstrates that pattern before you have asked for your first real feature. When the next prompt is "add Stripe" or "send an email when someone signs up", the fastest completion is the one that follows the file already in front of it, and a provider key lands beside the one that was always meant to be public.

Moving it to an environment variable does not help by itself. Vite inlines every VITE_-prefixed variable into the bundle at build time, so a secret in .env is a secret on your CDN.

The change that prevents it

Split credentials into two buckets before you prompt, and say which is which. The Supabase anon key is publishable by design — the control protecting that data is row-level security, not secrecy. Everything else (a service-role key, a Stripe secret key, any provider key that can spend money or read another user's data) must never reach the client at all: it belongs in a Supabase Edge Function or another server, with the browser calling that endpoint.

Paste this into your project instructions, or into the prompt itself:

Never put a secret in client code or in a VITE_-prefixed
env var — Vite inlines both into the browser bundle.
Any key that can spend money, send mail, or read another
user's data goes in a Supabase Edge Function and is read
from Deno.env.get() there. The browser calls the function.
The Supabase anon key is the only key allowed in the client,
and every table it can reach must have RLS enabled.

The scan that verifies it

Run a scan and confirm the count goes to zero. If a hit is the anon key or another value that is genuinely meant to be published, mark that line reviewed rather than deleting the rule.

npx xploitscan scan . -f json \
  | jq '[.findings[] | select(.rule == "VC001")] | length'

Honest caveat. Some of these are the Supabase anon key, which is publishable. That is exactly why the fix is to sort your keys rather than to silence the rule: the same check is what catches the service-role key sitting three lines below it.

highVC020 · 50 of 50 projects · 108 occurrences

The response headers a static host does not set for you

Missing Content Security Policy (CSP)

Why this workflow produces it

This is really three findings in the table above — the missing content-security policy, the missing frame-ancestors control, and the missing content-type and referrer policies. They travel together because they have one cause.

A Lovable project is a static index.html served by a host. All of these are response headers, and a static file does not carry a response header. Something in front of it has to add them, and the default deployment target usually adds none.

Nothing looks broken as a result, because none of these headers change how a working app behaves. They only change what happens during an attack, which is why they survive every round of testing you do before launch.

The change that prevents it

Add them where the host reads them: a _headers file on Netlify or Cloudflare Pages, the headers array in vercel.json on Vercel. The content-security policy is the one worth real time and the one most likely to break something, so run it in report-only mode first and read the reports before you enforce it. The rest are close to free.

The scan that verifies it

Scan for all three, then check the live response and confirm the headers actually arrive: curl -sI https://your-app.example. The scan reads your repository; only the response proves a header is really being sent.

npx xploitscan scan . -f json \
  | jq '[.findings[] | select(.rule == "VC020" or .rule == "VC027" or .rule == "VC056")] | length'

VC027 and VC056 are outside the free rule set. The free scan runs 30 of the 212 rules, so a free plan never evaluates them. This command also counts VC020, which a free scan does run — so the number it returns on a free plan is the VC020 count alone, and it says nothing either way about VC027 and VC056.

Honest caveat. This cluster fired on every project in the sample, which says more about the scaffold than about any developer — and part of it is a measurement limit on our side. The scanner reads your repository, so a host that already injects these headers still reads as missing. Check the live response before you spend an afternoon on it.

mediumVC037 · 19 of 50 projects · 749 occurrences

Error handlers that hand back the internals

Stack Traces Exposed in API Responses

Why this workflow produces it

Read the two numbers in the header of this section together. It reached a minority of projects and still fired hundreds of times, which is what a copy-pasted shape looks like: once per endpoint, in the projects that have endpoints at all.

The generated catch block is almost always the same three lines: catch the error, JSON.stringify a body containing error.message, return a 500. It is a completely reasonable thing to write while you are building, and nothing about the app misbehaves afterwards, so it never comes up again.

What ships to a caller is your ORM's error text, your table and column names, and often the query that failed — a free map of your schema for anyone who sends a deliberately malformed request.

The change that prevents it

Ask for the error contract explicitly, once, and it applies to every endpoint generated afterwards. Log the real error server-side, return a generic message and a correlation id to the caller.

Paste this into your project instructions, or into the prompt itself:

Every server error response returns a generic message and a
random correlation id, never error.message and never a stack.
Log the full error with that same id server-side so it can
still be traced. Apply this to every handler you generate.

The scan that verifies it

Scan, then confirm by hand: send a request that is guaranteed to fail and read the response body. It should tell you nothing you did not already know about your database.

npx xploitscan scan . -f json \
  | jq '[.findings[] | select(.rule == "VC037")] | length'

The one we are still bad at, stated plainly

Everything above is a finding we detect well. This section is the opposite: a separate research pass over the same sampling frame went looking for request handlers with no authentication check, and the honest result is that we currently flag a small minority of them in Lovable-shaped code.

171

handlers in the Lovable slice where the analysis could find no authentication check

13%

of those 171 that our shipped rules flag today — a point estimate, not the middle of the interval below

95% confidence interval 9%–19%

This is not a builder ranking. The research pass behind these figures states in its own limitations that it does not support comparing one builder against another, so this page reports its own slice and nothing relative to anyone else's.

For context from the same pass, across all four builders and 174 repositories: 51% [44%–58%] had any server-side handler at all, and of those, 69% [58%–77%] had at least one handler with no authentication check the analysis could find. Those two figures are round-level, not Lovable-specific, and are labelled that way on purpose — the research pass does not break them out per builder, so neither does this page.

What that does not mean

  • A shape is not a vulnerability. Nothing was exploited and no request was ever sent to a running application. A handler in this class is one where a static pass could not find an auth check. Some fraction are protected by means the method cannot see.
  • 47.5% of handlers were undecidable from a single file and were classified as such rather than guessed at. A handler goes to that bucket whenever protection might exist somewhere the analysis cannot look — middleware mounted before the route table, for instance. Every rate above excludes them, so these describe the decidable slice, not the whole corpus.
  • Handler counts are concentrated. A handful of large repositories contribute most of the handlers, so a handler-level percentage is closer to a statement about those projects than about a typical one.
  • Fewer than half of sampled projects have a server surface at all. Most Lovable projects are client-only against a hosted backend, where the control that matters is row-level security rather than a handler-level check.

We publish this because a scanner that only advertises what it catches is not much use for deciding whether to trust it. If your project has server-side handlers, read them yourself with one question in mind: who is this endpoint willing to answer? Our benchmark page carries the same disclosure in the other direction — a held-out access-control case that no scanner we test catches, including ours.

The findings we do not count against you

Every number above covers application code only. A Lovable project also arrives with a copied-in component library, and findings there are separated before any headline figure is computed. They are reported, never hidden, and they never touch a grade.

In this round 86% of the 50 projects (43 of 50) had a critical finding in that copied-in code — a higher rate than the 70% in their own code. That column is mostly our false positive. One shadcn/ui component sets CSS variables through a raw-HTML style block, which trips a critical cross-site-scripting rule. It is CSS-variable theming, not an injection sink, and a rule that flags it is our bug rather than yours or the builder's.

Because that one file is byte-identical everywhere it is copied, it shows up in almost every project that vendors the suite. Reading it as a security signal would mean ranking projects by how much of a component library they happened to copy in. This is also why your grade is computed on your own code by default — you can opt into the stricter view with xploitscan scan . --grade-vendored if you want everything counted.

Find out which of these are in your project

Same application-versus-scaffold split, run against your repository instead of someone else's. No credit card and no signup for the first scan — that one runs the 30 free rules rather than the 212 this round used, and the full set comes with a paid plan.

Numbers on this page are read from the published Vibe Security Index round 2026-09 at build time. Method and limitations: methodology.