---
title: "Vibe coding security risks: 5 holes I reproduced"
description: "What goes wrong in vibe-coded apps, with numbers from three studies and five access holes I reproduced on a local Supabase stack, plus how to check yours."
url: "https://madsalt.tech/blog/vibe-coding-security-risks/"
author: "Denys Zaichenko"
language: "en"
published: 2026-10-24
---

# Vibe coding security risks: 5 holes I reproduced

TL;DR

The code an AI writes usually runs. What fails is who can reach the data. In a 2026 audit of 200 deployed vibe-coded apps, 91% had at least one vulnerability and broken access control was the largest group. I reproduced five of the access holes on a local Supabase stack: with only the public key, a table without RLS, an always-true policy, a view and a leaked secret key each handed out all 3 of 3 rows. Check your app for these first.

I'm Denys, a full-stack and AI engineer. I fix apps that were built with Lovable, Bolt, Cursor or Claude Code, and most of them run on Supabase. This article collects what the published research says about vibe coding security, and what I saw when I rebuilt the common mistakes on purpose.

## What are the security risks of vibe coding?

The main risk is broken access control: data that any visitor can read or change because the rule that should stop them is missing, too loose, or bypassed. Injection and weak authentication come next. Three independent studies point the same way:

| Study                                                                                                                                     | What was tested                                                                     | What it found                                                                                                                                                            |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Deng, Fan and Meng, arXiv 2606.23130](https://arxiv.org/abs/2606.23130), June 2026                                                       | 200 deployed apps built with Claude Code and Lovable, audited by hand and by agents | 182 of 200 apps (91%) had at least one vulnerability; 65.77% of the 1,186 findings were Critical or High; broken access control was the largest category, in 64% of apps |
| [Escape](https://escape.tech/blog/methodology-how-we-discovered-vulnerabilities-apps-built-with-vibe-coding/), October 2025               | 5,600 public apps from Lovable, Base44, Create.xyz and others, passive scan         | 2,000+ high-impact vulnerabilities, 400+ exposed secrets, 175 cases of exposed personal data; main cause was misconfigured permissions, especially Supabase RLS          |
| [Veracode GenAI Code Security Report](https://www.veracode.com/resources/analyst-reports/2026-genai-code-security-report/), 2025 and 2026 | 80 coding tasks given to 100+ models                                                | Only 55% of tasks produced secure code in 2025; the average pass rate was 56% in 2026, flat across model sizes and years                                                 |

The studies measure different things, so the percentages do not add up to one number. What they share is the shape: the code works, and the gaps sit at the trust boundary.

## Which holes did I reproduce?

I set up one table, `orders`, with three rows of fake customers on a local Supabase stack (CLI 2.109.1, Postgres 17.6, PostgREST 14.14). Then I called the Data API the way a browser does, with the publishable key that every Supabase frontend ships, and changed one thing at a time.

![What one public key got from one table. Local Supabase, 3 rows of fake orders, one request each. Table without RLS: Publishable key → orders, RLS off → 3 of 3 rows; RLS on, no policy yet: Publishable key → orders, RLS on → 0 rows; Policy using (true): Publishable key → using (true) → 3 of 3 rows; View without security_invoker: Publishable key → order_summary view → 3 of 3 rows; Secret key in the browser code: Secret key → BYPASSRLS → 3 of 3 rows.](https://madsalt.tech/charts/en/vibe-coding-security-risks.svg)

*Figure 1. My reproduction on a local Supabase stack (Postgres 17.6, PostgREST 14.14), 23 October 2026.*

| Setup                         | What the public key got                           | Why                                                                                        |
| ----------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Table without RLS             | All 3 rows, and an insert came back `201 Created` | The default grants let `anon` read and write, and nothing filters rows                     |
| RLS on, no policy             | `[]`, an empty list                               | RLS with no policy denies everything to `anon` and `authenticated`                         |
| Policy `using (true)`         | All 3 rows                                        | The policy exists, so the table looks protected, but it allows every row                   |
| View over the protected table | All 3 rows                                        | A view runs with its owner's rights unless it has `security_invoker = true`; with it, `[]` |
| Secret key in frontend code   | All 3 rows                                        | The secret key maps to `service_role`, which has `BYPASSRLS`                               |

The second row is the dangerous moment. RLS is on and the app shows an empty list, so it looks broken. The quickest prompt that "fixes" it is a policy that allows everything, which is the third row. That loop, error, then a fix that widens access, is the pattern I meet most in vibe-coded apps, and it is why "RLS is enabled" says little on its own.

## Why does AI-written code fail at access control?

An agent is rewarded for code that runs. An empty list or a `401` looks like a bug, and the fix that makes it disappear is often the one that opens access. The arXiv study traces its 1,186 findings to eight recurring failure modes, grouped as memory, objective and knowledge defects of the agents, and it found that better prompting reduced the number of holes but did not remove them.

The second reason is architecture. A Lovable or Bolt app usually has no backend of its own: the browser talks straight to the database through the Data API. Every rule that protects customer data has to live in Postgres, as grants, policies and functions, and none of that is visible in the app preview, where you are signed in as the owner. Supabase is changing one default here: from [30 October 2026](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically), new tables in existing projects are no longer granted to the Data API automatically. On my stack, a table without the grant answered `42501 permission denied` with a hint that names the missing `GRANT`. That stops the first row of the table above for new tables, but not the other four, and not tables that already exist.

## What else goes wrong outside the database?

The same studies list risks that have nothing to do with RLS:

- **Secrets in the bundle.** Escape found 400+ exposed secrets, including OpenAI keys and payment admin keys. Anything shipped to the browser is public.
- **Route guards only in React.** A page that redirects signed-out users still has an API behind it that answers anyone.
- **Rate limits that never fire.** On a hosted Supabase project I probed, a counter kept in an edge function's memory reset on every request. I wrote that up in my [Lovable app security checklist](https://madsalt.tech/blog/lovable-app-security-checklist/).
- **Old libraries.** Escape counted 254 vulnerable JavaScript library instances in public assets, often inherited from templates.

## How do you check a vibe-coded app?

1. Open the app, open DevTools → Network, and find requests to `/rest/v1/`. Copy one as cURL, remove the `Authorization` header, keep the `apikey`, and run it. If data comes back, so it does for anyone.
2. Search the built JavaScript for `sb_secret_` and `service_role`. A match is a key to rotate today.
3. Run the free [Supabase security check](https://madsalt.tech/tools/supabase-security-check/). It reads the project's catalog and flags, among its 46 rules, "RLS disabled on exposed tables", "Policies that allow every row", "Views that bypass RLS" and "Default privileges that open new objects to the API": the first four rows of the table above.
4. Test with two users in two companies, the one test a scanner cannot do for you. How I do it is in the [Lovable checklist](https://madsalt.tech/blog/lovable-app-security-checklist/), and [how to test Supabase RLS](https://madsalt.tech/blog/how-to-test-supabase-rls/) shows the method on a production app with a permission screen.

```bash
curl 'https://<project>.supabase.co/rest/v1/orders?select=*' \
  -H 'apikey: <the publishable key from your page>'
```

## What this does not show

My reproduction is one table on a local stack, built to show each mechanism, not a scan of real apps. The prevalence numbers come from the three studies, each with its own sample and method, linked above. And a clean result on these five checks does not make an app safe: logic bugs, such as a guard that lets a missing value through, need tests of their own. One of those is in [a NULL that skipped a permission check](https://madsalt.tech/notes/plpgsql-if-not-null-fails-open/).

Built your app with an AI tool?

Happy to compare notes on what to check first. No pitch.

[Get in touch](https://madsalt.tech/#contact)
