9 min read

The 3 AM Lovable panic — what to do when you see a leak tweet

It's 3 AM. You scrolled X and saw a tweet about a Lovable / Bolt / v0 app leaking customer data. You start wondering if yours is next. Here is the exact checklist to run in the next 30 minutes — what to check, what to fix first, and how to stop having this problem.

You built your app with Lovable, Bolt, v0, Cursor, or Replit. It works. People are using it. You went to bed feeling good about it.

Then you opened X and saw a thread about a vibe-coded app that leaked every customer's data through a missing Supabase RLS policy. The replies are full of "is yours next?" The thread has 12K likes.

Now it's 3 AM. You can't sleep. You don't know if your app has the same bug.

This is the checklist. Run it in the next 30 minutes. If you find the bug, you have a fix path. If you don't, you can sleep.

Step 1 — Pull up your Supabase project

Open your Supabase dashboard. Go to AuthenticationPolicies. You're going to look at every table.

If you see any table with the RLS Disabled badge, that's your problem. That table is readable by anyone with your anon key. The anon key ships in your client bundle by design — every visitor has it.

Fix in 60 seconds: click the table → Enable RLS. Then add at least one policy. The minimum-viable policy for a typical user-owned table is:

create policy "Users see their own rows" on public.your_table
  for select using (auth.uid() = user_id);

Repeat for insert, update, delete policies as needed. Default-deny by enabling RLS without a policy is a valid starting position — it locks the table while you write the right policies.

Step 2 — Check if your service-role key shipped to the client

This is the catastrophic version of the RLS bug. The service-role key bypasses every RLS policy you wrote. If it shipped client-side, your database is fully open regardless of your policy work.

How to check (60 seconds):

1. Open your repo in your editor. 2. Search for the literal string service_role and SUPABASE_SERVICE_ROLE_KEY. 3. For every match, ask: is this file rendered or imported by anything that runs in the browser?

In Next.js, the rule of thumb: anything in app/ or pages/ that does NOT have "use server" at the top, anything in components/ that doesn't, anything in lib/ that's imported by either — that code runs in the browser. The service-role key cannot live there.

If you find a service-role key in client code: rotate it immediately. Supabase dashboard → Settings → API → reset service_role key. Update your server-side env vars. Redeploy.

Step 3 — Check for keys in your git history

If a service-role key was in your repo at any point (even briefly, even in a commit you "fixed" later), assume it's compromised. GitHub's secret scanning catches some patterns; attackers' bots catch the rest.

git log -p --all -S 'eyJ' | head -200

Scan that output for anything starting with eyJ followed by a long base64 string. That's a JWT pattern — every Supabase key starts with it. If you see a service-role key (the role claim in the decoded JWT will say service_role), rotate it.

Same for: OpenAI keys (sk-...), Stripe keys (sk_live_..., sk_test_...), GitHub PATs (ghp_...), AWS keys (AKIA...).

Step 4 — Check for the broken-auth bug everyone has

If your app has a route that returns user-specific data, there's a high chance the AI generated it without proper authorization (see the 92% data point for the underlying research). The pattern looks like:

// app/api/orders/[id]/route.ts
export async function GET(req, { params }) {
  const order = await db.from('orders').select('*').eq('id', params.id).single();
  return Response.json(order);
}

The bug: any authenticated user can read any other user's order by changing the URL. The fix is comparing the order's owner against the authenticated user:

```ts export async function GET(req, { params }) { const { data: user } = await supabase.auth.getUser(); if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 });

const order = await db.from('orders').select('*').eq('id', params.id).single(); if (order.data?.user_id !== user.id) { return Response.json({ error: 'not found' }, { status: 404 }); } return Response.json(order); } ```

(Return 404 not 403 — don't tell the attacker the resource exists.)

This bug class has names: BOLA (Broken Object Level Authorization), IDOR (Insecure Direct Object Reference). It's the most common bug in AI-generated apps. Search your routes for any params.id that hits the database without comparing ownership.

Step 5 — Check your middleware actually runs

If you have middleware.ts that's supposed to protect /api/admin/* or /api/private/*, verify the matcher actually matches. The common bug:

// middleware.ts
export const config = { matcher: '/admin/:path*' };  // matches /admin/...
// but your protected routes are at /api/admin/... — middleware doesn't run!

Test it: curl https://your-app.com/api/admin/users from an incognito window. If the response is the protected data instead of a redirect or 401, your middleware is not protecting it.

Bonus check: make sure you're on Next.js 15.2.3+ (or 14.2.25+, 13.5.9+, 12.3.5+). If you're on an older version, you have CVE-2025-29927 — a critical middleware bypass via a single HTTP header. Upgrade now. Full write-up.

Step 6 — Stop having this problem

You can't run this checklist every time someone tweets about a leak. The whole point of vibe-coding is that you don't want to think about security every day.

Securie is the autonomous security engineer for your AI-built app. Install the GitHub App in 2 minutes. On every PR, three Day-1 specialists run automatically:

  • Supabase RLS specialist — reads every create policy statement, runs your policies in a sandbox, tells you which ones leak rows
  • Secret scanner — detects every leaked credential pattern, live-validates against the provider API, and (Indie tier and up) opens an auto-rotation PR
  • Broken auth specialist — reads every route that uses params.id, runs a cross-user request in a sandbox, tells you which ones return another user's data

Findings only ship if the sandbox successfully reproduces the exploit. No noise, no triage, no false positives.

After tonight

Go back to bed. Tomorrow morning, install Securie and let it run on your codebase. You will probably find one or two issues you missed. That's fine — that's the point. The bugs you fix the morning after the panic are the bugs that don't get tweeted about in a thread with 12K likes.

Related

Related posts