# Vulnerabilities in apps built with v0, Lovable, and Bolt — the AI app-builder pitfalls and a pre-launch inspection

> A guide to closing, under your own responsibility, the vulnerabilities in already-launched apps with real users built by AI app builders like v0, Lovable, and Bolt. Read the real Lovable case CVE-2025-48757 (RLS gap, CWE-863, CVSS 9.3), then inspect the exported code with scan → hand-checking RLS → probe.

- Published: 2026-07-07
- Author: 友田 陽大
- Tags: セキュリティ, バイブコーディング, AI駆動開発, Supabase, RLS
- URL: https://tomodahinata.com/en/blog/v0-lovable-bolt-ai-app-builder-security-vulnerability-guide
- Category: Vibe coding: shipping AI-generated code safely
- Pillar guide: https://tomodahinata.com/en/blog/vibe-coding-what-is-tools-risks-production-guide

## Key points

- v0, Lovable, and Bolt add auth and UI automatically, but row-level authorization (Supabase RLS) — 'who may read that row' — is not generated, or is left loose. So apps that 'authenticate but don't authorize' get shipped as-is.
- The real case that recorded this in the NVD is CVE-2025-48757 (Lovable, through 2025-04-15: an RLS gap let unauthenticated attackers read/write arbitrary tables, CWE-863, CVSS 9.3 CRITICAL). The vendor disputed it as 'data protection is each user's responsibility' — the responsibility boundary is the whole point.
- Already launched with real users? It's not too late. Inspect the exported code and migrations with scan → hand-check RLS → probe, and you can mechanically close the most frequent holes (no RLS, USING(true), IDOR).
- The anon key being visible to the client is normal by design. What's dangerous is when the anon key is exposed while RLS is not correct. The service_role key bypasses RLS entirely, so never expose it to the client — the defense boundary is not the key but RLS.
- `npx @aegiskit/cli scan` automates horizontal controls and keeps vertical risks like authorization/IDOR and RLS design to detect-and-warn. Closing them is a human design decision; the tool complements that, it doesn't replace it.

---

Let me state the conclusion first. **AI app builders like v0, Lovable, and Bolt spit out a "working app" in minutes, but many of them "authenticate but don't authorize" — that is, login is implemented, yet the boundary of "who may read and write that row" (Supabase RLS) is left open when the app is launched.** And this is not a hypothesis. The RLS gap in a Lovable-built site is registered in the NVD as **CVE-2025-48757 (CWE-863, CVSS 9.3 CRITICAL)** — a flaw that let an unauthenticated attacker read or write arbitrary tables.

I'm on the side that has built production SaaS solo × generative AI (Claude Code) myself. So I don't deny the speed of builders. What I want to say is the opposite — **if you're already launched with real users, what you should do today is inspect the exported code "under your own responsibility."** This article distills that inspection into three steps — `scan → hand-check RLS → probe` — and shows it with real code, from a developer's perspective.

For how to diagnose "vulnerabilities of AI-generated code in general" across four layers (SCA / secrets / SAST / DAST), see [AI-generated code vulnerability assessment (2026 edition)](/blog/ai-generated-code-vulnerability-assessment-vibe-coding-security-guide); for the buyer's-view productionization (hardening), see [the vibe coding productionization guide](/blog/vibe-coding-ai-generated-code-production-hardening-guide). This article narrows to **the holes specific to "builder-built × already launched."**

---

## 1. What builders build, and what they don't

First, let me honestly carve out each tool's coverage. v0, Lovable, and Bolt differ in kind, but **the one thing they share is "they don't build your app's specific authorization."**

| Builder | Mainly builds | Doesn't build / leaves loose |
|---|---|---|
| v0 (Vercel) | UI components / screens | data-access authorization, RLS, server-side validation |
| Lovable | full-stack scaffold + Supabase connection | row-level owner scope (the contents of the RLS policy) |
| Bolt | full-stack scaffold + DB connection | tenant isolation, IDOR countermeasures, state-transition validation |

What builders add for you automatically is, in Aegis's terms, **the plumbing close to "horizontal controls," plus the happy-path UI and login.** A login screen appears, sign-up goes through, your own data shows up — up to here it's astonishingly fast.

The problem is what's beyond that — the **"vertical risk."** "If logged-in user A sends a legitimate request pointing at user B's ID, does B's data not come back?" This is not authentication (who you are) but authorization (what you may do), and because it **depends on the app-specific 'who owns what,'** builders can't fill it in as a matter of principle. Object-level authorization flaws (IDOR/BOLA) have stayed at [#1 in the OWASP API Security Top 10 since 2019](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/) precisely because this is the place that "doesn't close unless a human designs it."

---

## 2. The real case: reading CVE-2025-48757 (Lovable) from primary sources

To avoid ending in abstractions, let me cite the NVD's primary source. The description of CVE-2025-48757 reads, in the original:

> "An insufficient database Row-Level Security policy in Lovable through 2025-04-15 allows remote unauthenticated attackers to read or write to arbitrary database tables of generated sites."

Let me drop the key points into a table (source: [NVD — CVE-2025-48757](https://nvd.nist.gov/vuln/detail/CVE-2025-48757)).

| Item | Value |
|---|---|
| CWE | CWE-863 Incorrect Authorization |
| CVSS 3.1 | 9.3 CRITICAL (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N) |
| Affected | Lovable through 2025-04-15 |
| Published | 2025-05-29 |
| Status | Disputed (vendor disputes it) |

Three things to read out of this:

1. **The precondition is "unauthenticated (PR:N)."** That is, arbitrary tables were within reach without even logging in. This is the textbook case of "authorization is open outside authentication," exactly the [structure of "authenticates but doesn't authorize"](/blog/supabase-rls-authenticated-vs-authorized-owner-scope-guide).
2. **The root cause is an "insufficient RLS policy."** Whether RLS was turned off entirely or was a wide-open policy like `USING (true)` differs by incident, but both converge on the single point that the design of "scoping rows to the owner" was missing.
3. **The status is Disputed** — the vendor argues that "protecting the data of generated apps is each user's responsibility." This isn't an excuse; it should be read as **a declaration of the responsibility boundary.** The builder makes the plumbing, but row-level authorization is "your responsibility" — the CVE officially says so.

That's exactly why the "inspect it yourself" of the following chapters is the crux.

---

## 3. Why builder-built apps leak right here

The structural reasons boil down to three.

### ① "Login works" performs "looks safe"

The moment you wire up Supabase Auth and a sign-in screen appears, many people feel "I could authenticate, so it's protected." But authentication is merely **identity checking at the entrance.** Handing out entry passes and locking each room are different jobs. If the lock called RLS is weak, a legitimate entry pass (a valid login, sometimes anonymous access) gets you into every room.

### ② The anon key is handed out "on the premise it goes to the client"

Supabase's anonymous (anon) key is a public key premised on being embedded in the browser. **That itself is as designed; exposure ≠ vulnerable.** What's dangerous is the state of "the anon key being handed out to the world while RLS is not correct." The defense boundary is not the secrecy of the key but **the contents of the RLS policy.** Conversely, if the `service_role` key, which bypasses RLS entirely, slips into the client side or a builder preview, that's an instant fail — [as Supabase officially states](https://supabase.com/docs/guides/database/postgres/row-level-security), service_role runs with PostgreSQL's `BYPASSRLS` and ignores RLS entirely.

### ③ A builder's default is optimized for "works first"

The first goal of the output is "the demo runs." So tables with no RLS, or wide-open `USING (true)` policies, tend to remain unquestioned. This isn't a gut feeling — it's a trend visible in a primary study of public Supabase apps:

> In a [primary study of 1,000 public Supabase apps](/blog/supabase-rls-security-field-study), about 9.2% of apps with RLS had a policy that "authenticates but doesn't scope rows to the owner" (a lower bound, since the sample is public repositories; owner-unscoped ≠ instantly vulnerable, needs confirmation).

"About 1 in 10 as a lower bound," and that's the figure for **public repositories with relatively higher security awareness.** Among real products built with a builder and launched while staying private, I'd estimate it's felt to be higher than this.

---

## 4. Drawing the responsibility boundary correctly — the substance of "your responsibility"

You don't need to take CVE-2025-48757 being Disputed negatively. If anything, it's **clear as a contract.** Diagrammed, the boundary looks like this.

```text
[ ビルダー/プラットフォーム側 ]        [ あなた（開発者/事業者）側 ]
  ・UI・画面の生成                       ・行レベルの認可設計（RLS の中身）
  ・Auth（サインイン）の配線             ・auth.uid() による所有者スコープ
  ・ホスティング・足回り                 ・service_role 経路の所有権チェック
  ・anon キーの発行                      ・テナント分離・IDOR 対策
  ─────────────────────                  ─────────────────────
  「動く」までを速く                     「漏れない」を担保する
```

In other words, the decision to use a builder itself is fine. **But you need the awareness that "the moment you export/launch, the entire right column moves to you."** Filling this in is the inspection procedure in the next chapter.

---

## 5. The post-export inspection procedure: scan → hand-check RLS → probe

On the premise that you're already launched with real users, here are the three steps that **close the most impactful holes first.** The order has meaning — suspect statically, confirm the meaning of authorization with your eyes, back it up dynamically.

### Step 1. scan mechanically for "horizontal holes" and "RLS suspicions"

Right in the root of the exported code (or the repo the builder connected), run static analysis first. No install, no config.

```bash
# いまのプロジェクトをそのまま静的解析する
npx @aegiskit/cli scan
```

What `scan` picks up are the holes frequent in builder-built apps:

- **Tables with no RLS / wide-open `USING (true)` policies / missing `WITH CHECK`** (reads `supabase/migrations/**.sql`)
- **Leaked secrets** (whether a `service_role` key or token is baked into the code)
- **Missing security headers / CSP, rate limiting, CSRF** (= horizontal controls)
- **Tainted input without an ownership scope = suspected IDOR** (tracked with source→sink data flow)

What's important here is that Aegis draws the line honestly. **Horizontal controls** like headers and rate limiting are subject to automation and auto-fixing, but **vertical risks** like authorization/IDOR and RLS design are **kept to "detect and warn."** It never claims to have fixed what it can't. So read `scan`'s output as a "list of suspicions," not "proof of safety."

### Step 2. Read RLS by hand — confirm "authentication" and "authorization" separately

The RLS that `scan` flags is, in the end, for a human to confirm the **meaning.** The machine looks at the *shape* of the policy, but it doesn't know the **business-rule meaning** of "is this table intended to be shared, or owner-only?" There's one thing to read — **whether rows are scoped to the owner with `auth.uid()`.**

```sql
-- ❌ ありがちなビルダー既定：認証はするが、行を所有者に絞らない
--    ログインさえしていれば、他人の行も全部読める（IDOR の温床）
create policy "read" on public.orders
  for select
  using ( auth.role() = 'authenticated' );

-- ✅ 所有者スコープ：自分が所有する行だけを読める
create policy "read own" on public.orders
  for select
  using ( auth.uid() = user_id );

-- ✅ 書き込みは USING だけでなく WITH CHECK も要る（なりすまし INSERT を止める）
create policy "insert own" on public.orders
  for insert
  with check ( auth.uid() = user_id );
```

`USING (true)` or `auth.role() = 'authenticated'` is **not always wrong.** If the intent is "this table really is shown to everyone," like public posts on a message board, it's correct. So the criterion is one thing only — **"is this table intended to be public?"** If the intent is "owner-only," it needs a scope via `auth.uid()`. This "authentication vs. authorization" split, and the write bypass when you drop `WITH CHECK`, are each covered in detail in [the authenticates-but-doesn't-authorize problem](/blog/supabase-rls-authenticated-vs-authorized-owner-scope-guide) (not repeated here).

Alongside, visually check these "builder-specific" items too:

| Where to look | What to confirm |
|---|---|
| Where the `service_role` key lives | whether it's exposed to the client, preview, or a public repo (rotate immediately if so) |
| SECURITY DEFINER functions | whether `search_path` is pinned (unpinned is an entry point for privilege escalation) |
| Edge Functions / server processing | whether ownership is re-checked internally even for anon-routed calls |
| Tables with no RLS | a "just created" table is exactly where a blank policy lingers |

### Step 3. probe to back up the "suspicions" at runtime

Static analysis is "suspicion"; dynamic analysis is "confirmation." Finally, send a non-destructive probe to **a staging environment you own** and check whether the suspicions found in Steps 1–2 actually reproduce.

```bash
# 自分のアプリ（所有物）へ、安全・スコープ固定・非破壊で裏取りする
# テストIDを渡すと missing-auth / IDOR の再現も確認できる
npx @aegiskit/cli probe https://staging.example.com --correlate
```

I'll stress this hard — **the only thing you may point probe at is an environment you own and control.** Sending it to someone else's site without permission can constitute unauthorized access, even if non-destructive. probe is designed to fall to the safe side (localhost default, scope-pinned, with a request budget), but **"only against what you own"** is the operator's responsibility.

Fix only what reproduced, as a "confirmed exposure," at top priority — this static × dynamic correlation is the crux of crushing only the real harm without drowning in noise.

### And then pin it in CI

To keep a hole you closed from recurring, wire `scan` into the CI gate. Only high-confidence detections fail the build, and SARIF stays in GitHub code scanning. Every time you "regenerate" with a builder, the machine stops the same hole from reopening. The CI gate's design philosophy is in [the quality gate for AI-driven development](/blog/ai-driven-development-quality-gates-ci-type-safety-test-security), and the approach of pinning authorization in the spec before generation is in [the spec-driven development workflow](/blog/spec-driven-development-claude-code-ai-agent-production-workflow).

---

## 6. The honest scope — what the tool "doesn't do"

Finally, to avoid breeding complacency, let me spell out the boundary. **"I ran scan, so it's safe" is the mindset that produces the worst security outcomes.**

- `npx @aegiskit/cli scan` **automates horizontal controls** and **keeps vertical risks (authorization/IDOR, RLS design, tenant isolation) to detect-and-warn.** Closing them is your design decision.
- A clean result means "you haven't stepped on the common traps," not "this code is safe."
- Data-flow analysis is mostly intraprocedural (within a function); flows that cross modules or frameworks can be missed.
- It **does not replace** RLS design, code review, threat modeling, and manual penetration testing — **it complements** them.

Even so, the value of mechanically closing the most frequent holes is large. Without killing the builder's speed, you can systematically close the "authenticates but doesn't authorize" state even after launch. Start with the free OSS, one command against your current project — [Aegis (`npx @aegiskit/cli scan`)](/aegis). And if the **vertical risks** that `scan` surfaced need real design work to close, that's where I come in.

## Summary

- v0, Lovable, and Bolt build fast up to "it works," but **row-level authorization (RLS) is your responsibility.** CVE-2025-48757 (Lovable, CWE-863, CVSS 9.3, Disputed) officially shows that responsibility boundary.
- Already launched with real users? It's not too late. With three steps — **scan (suspect statically) → hand-check RLS (confirm the meaning of authorization) → probe (back it up against what you own)** — you can mechanically close the most frequent holes.
- **Anon key exposure is normal; what's dangerous is exposure while RLS stays loose.** Never expose the service_role key to the client. The defense boundary is not the key but RLS.
- The tool automates horizontal controls and keeps vertical risks to detect-and-warn only. **Closing them is human design** — the tool doesn't replace it, it complements it.
