DEV Community

Cenk KURTOĞLU
Cenk KURTOĞLU

Posted on

4 Security Holes Your AI-Generated Supabase App Has (Lovable, Cursor, Bolt, v0)

4 Security Holes Your AI-Generated Supabase App Has (Lovable, Cursor, Bolt, v0)

You prompted: "Build me a SaaS with auth, database, and billing."

The AI delivered. Green checks everywhere. You deployed.

But did it secure the data layer?

I've audited 50+ vibe-coded Supabase apps. The same 4 holes appear in ~80% of them — not because AI is "bad at security," but because security requires adversarial thinking, not just implementation.


Hole 1: Service Role Key in Client Bundle

The code AI writes:

// lib/supabase.ts
export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.SUPABASE_SERVICE_ROLE_KEY  // 💀 COMMITTED TO REPO
)
Enter fullscreen mode Exit fullscreen mode

Why it happens: AI sees SUPABASE_SERVICE_ROLE_KEY in .env.example, assumes it's for the client. It's not — it's root database access.

Impact: Anyone extracts the key from your JS bundle → full DB read/write/delete, manage auth users, access storage.

Fix:

  • Server-only: supabaseAdmin in Edge Functions / API routes only
  • Never NEXT_PUBLIC_ prefix on service role
  • Rotate immediately if ever committed: Supabase Dashboard → Settings → API → "Reset service_role key"

Hole 2: RLS Enabled, But using (true) Policy

The migration AI generates:

ALTER TABLE documents ENABLE ROW LEVEL SECURITY;

CREATE POLICY "enable_read" ON documents FOR SELECT
  USING (true);  -- 💀 EVERYONE READS EVERYTHING
Enter fullscreen mode Exit fullscreen mode

Why it happens: AI gets "permission denied" error during dev, adds permissive policy to unblock. Forgets to tighten.

Impact: Green "RLS Enabled" shield in dashboard. anon role reads all rows. Multi-tenant = cross-tenant leak.

Fix:

-- Owner-only read
CREATE POLICY "owner_read" ON documents FOR SELECT
  USING (auth.uid() = user_id);

-- Multi-tenant read
CREATE POLICY "org_read" ON documents FOR SELECT
  USING (org_id = current_org_id());
Enter fullscreen mode Exit fullscreen mode

Hole 3: Missing Write Policies (Silent Data Poisoning)

What AI implements:

-- Only SELECT policy
CREATE POLICY "owner_read" ON documents FOR SELECT
  USING (auth.uid() = user_id);
Enter fullscreen mode Exit fullscreen mode

What's missing: INSERT, UPDATE, DELETE policies.

Impact: Any authenticated user can:

  • INSERT rows with user_id = victim_id → impersonation
  • UPDATE their row → change user_id to victim's → own victim's data
  • DELETE any row → ransomware

All silently succeed (RLS filters on read, not write, without explicit policies).

Fix: Complete command coverage:

CREATE POLICY "owner_all" ON documents
  FOR ALL USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);
Enter fullscreen mode Exit fullscreen mode

Hole 4: Middleware Doesn't Forward Auth Cookies (Vercel Double Login)

The middleware.ts AI writes:

export async function middleware(request: NextRequest) {
  const supabase = createServerClient(/* ... */)
  const { data: { user } } = await supabase.auth.getUser()
  // user is null in production! 💀
}
Enter fullscreen mode Exit fullscreen mode

Why it happens: Works locally. On Vercel, cookies aren't forwarded to Supabase client → getUser() returns null → auth breaks → users logged out randomly.

Fix: Explicitly forward auth cookies:

export async function middleware(request: NextRequest) {
  const response = NextResponse.next()
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return request.cookies.getAll() },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => 
            response.cookies.set(name, value, options)
          )
        }
      }
    }
  )
  await supabase.auth.getUser()  // Refreshes session, writes cookies to response
  return response
}
Enter fullscreen mode Exit fullscreen mode

The Pattern: AI Optimizes for "Working" Not "Secure"

Dev Goal AI Optimizes For Security Gap
"Make auth work" getUser() returns user Cookie forwarding on Vercel
"Fix permission denied" Policy that allows the query using (true) = open door
"Add admin features" Service role key usage Key in client bundle
"Enable RLS" ALTER TABLE ... ENABLE RLS Only SELECT policy

Free: Audit Your App in 30 Seconds

git clone https://github.com/cekuu35/supabase-rls-leak-demo
cd supabase-rls-leak-demo
npm install
npm run audit
Enter fullscreen mode Exit fullscreen mode

47 automated checks including all 4 holes above + 43 more:

  • Service role key in client code
  • RLS coverage per table
  • Write policy completeness (WITH CHECK)
  • Cookie forwarding in middleware
  • Grant/policy mismatches
  • SECURITY DEFINER function safety

CTA

Free: Run audit → see your holes

$29 Kit: 47-check checklist + remediation SQL per finding

$50/mo Guard: CI gate fails build on any RLS gap


Previous in series: "Supabase RLS Not Working? Fix It by Symptom" | "RLS Write-Leak: The Silent Data Exposure"

Top comments (0)