FOR PEOPLE SHIPPING LOVABLE APPS

What Lovable apps get wrong

In round 2026-07 of the Vibe Security Index we scanned 25 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.
  • 25 repositories, drawn from a universe of 10,284. Every rate on this page is stated with the count behind it, because a percentage over 25 is a different kind of object than a percentage over 25,000. 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-07, generated July 27, 2026. Full method, sampling frame and limitations: Index methodology.

The shape of the sample

76%

had at least one critical finding in their own code

19 of 25

100%

had at least one high or worse

25 of 25

51

median findings per project, application code only

across 25 projects

160

median scanned files per project

size is a confound on every rate above

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 (834 occurrences across 25 of 25 projects), Console.log Left in Production Code (824 occurrences across 21 of 25 projects) and N+1 Query Pattern Detected (226 occurrences across 13 of 25 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 17 of 25 projects.

Security findings that repeat, application code only

From round 2026-07. n = 25 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 SecretVC001critical68%17 of 25154
Missing Content Security Policy (CSP)VC020high100%25 of 2580
Session Fixation RiskVC046high60%15 of 2524
High-Entropy String Detected (Possible Secret)ENTROPYhigh44%11 of 2583
Missing Security Meta Tags / HeadersVC027medium100%25 of 25161
Clickjacking — Missing X-Frame-OptionsVC056medium100%25 of 2590
Stack Traces Exposed in API ResponsesVC037medium48%12 of 25572

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 · 17 of 25 projects · 154 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.

highENTROPY · 11 of 25 projects · 83 occurrences

Secrets that do not look like anyone's secrets

High-Entropy String Detected (Possible Secret)

Why this workflow produces it

The secrets rule above knows the shape of the common providers. This check is the backstop for everything else: a webhook signing secret, an internal token, a private key pasted into a constant — strings with no recognisable prefix that are nonetheless clearly not prose.

It matters most in exactly this workflow, because a project assembled by prompting accumulates integrations faster than anyone writes down which of their values are safe to publish.

The change that prevents it

Same rule as above: decide what is publishable before the key is pasted anywhere. If a value has to be in the client, it must be one that is safe to hand to a stranger.

The scan that verifies it

Scan, then triage the list once. Anything genuinely public — a published verification token, a hash, encoded data — gets an inline ENTROPY-OK comment with the reason, and never fires again.

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

Honest caveat. High-entropy strings also appear in vendored component libraries, where they are not secrets at all. This page counts only findings in application code; the copied-in scaffold is broken out separately below.

highVC020 · 25 of 25 projects · 80 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'

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.

highVC046 · 15 of 25 projects · 24 occurrences

Sign-in paths with no visible session rotation

Session Fixation Risk

Why this workflow produces it

The sign-in screen is usually one of the first things generated, and it is written as a form handler: call the auth method, then navigate. Whether the session identifier the browser was carrying before sign-in is replaced afterwards is not something the generated code ever addresses, because nothing in the happy path depends on it.

When the identifier is not replaced, a value an attacker managed to plant in the browser beforehand survives into the authenticated session.

The change that prevents it

State the requirement in the prompt when you ask for auth, and again if you ever hand-roll a session rather than delegating to the SDK.

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

On successful sign-in, issue a brand-new session identifier
and discard the pre-login one. Never carry a session value
that existed before authentication into the signed-in state.

The scan that verifies it

Scan, then read the sign-in path once with this specific question in mind. If your auth SDK already issues a fresh session on sign-in, mark the finding reviewed with an inline comment giving that reason — a documented decision, not a deleted rule.

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

Honest caveat. This check reads one file at a time and cannot follow session handling into an SDK. In a project that delegates sessions entirely to a hosted auth provider, a share of these are ours to answer for rather than yours. It is on this list because the sign-in path is worth five minutes of your reading, not because every hit is a confirmed bug.

mediumVC037 · 12 of 25 projects · 572 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

95% confidence interval 9%–19%

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.
  • About 47% 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 88% of the 25 projects (22 of 25) had a critical finding in that copied-in code — a higher rate than the 76% 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 212 rules, same application-versus-scaffold split, run against your repository instead of someone else's. No credit card, no signup for the first scan.

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