Security Guides

Supabase RLS Is Not Enough: How Vibe-Coded Apps Get Hacked

SecuriSky TeamApril 9, 20267 min read

The Problem With "I Enabled RLS"

When developers enable Row-Level Security in Supabase, they often assume they're protected.

They're not — not by default.

RLS blocks direct PostgREST access, but it doesn't protect your:

  • Edge Functions that run as service_role
  • Server-side API routes that bypass RLS entirely
  • Misconfigured policies that have USING (true) accidentally
  • How Vibe-Coded Apps Break This

    When you use Cursor AI, Lovable, or Bolt to build a full-stack app, the AI generates Supabase Edge Functions that run with the service role key — which bypasses all RLS policies.

    Here's a real pattern the AI generates:

    // ⚠️ This bypasses ALL RLS policies
    

    const supabase = createClient(

    process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! // ← this is the problem

    );

    export async function getUserData(userId: string) {

    const { data } = await supabase .from('users') .select('*') .eq('id', userId); return data;

    }

    If this function is called from an API route without validating the calling user's session, any authenticated user can fetch any other user's data by simply passing a different userId.

    The Three RLS Bypass Patterns

    1. Service Role Key Exposure

    The service_role key is meant for server-side admin tasks only. It should never be used in:

  • Client-side code
  • Publicly accessible API routes without auth checks
  • Shared between multiple functions without scoping
  • Detection: Search your codebase for SUPABASE_SERVICE_ROLE_KEY and audit every call site.

    2. Policies With USING (true)

    Cursor and other AI tools sometimes generate placeholder policies that allow everything:

    -- ⚠️ This policy allows anyone to read everything
    

    CREATE POLICY "Enable read access for all users"

    ON public.users

    FOR SELECT

    USING (true); -- ← dangerous default

    Fix: Replace USING (true) with USING (auth.uid() = user_id) or an appropriate condition.

    3. Missing DELETE/UPDATE Policies

    Most developers add SELECT policies but forget INSERT, UPDATE, and DELETE. Supabase defaults to deny all for missing policies — but this protection breaks when you use the service role key.

    The SecuriSky Detection

    Our scanner specifically probes for:

  • API routes that accept a userId parameter without verifying it matches request.user.id
  • JavaScript bundles containing service_role references
  • Supabase project settings that indicate public schema access
  • A score of A or B on the free tier doesn't mean your RLS is correctly configured — we only test 3 of 18 attack surfaces on the free plan.

    Quick Fix Checklist

  • [ ] Audit every use of service_role key — restrict to server-side admin only
  • [ ] Add auth.uid() = user_id to all RLS policies
  • [ ] Enable RLS on all tables, not just users
  • [ ] Add policies for INSERT, UPDATE, DELETE — not just SELECT
  • [ ] Use Supabase's RLS policy tester in the dashboard
  • Next Steps

    Run a full SecuriSky scan to detect RLS misconfigurations automatically, including AI-generated patterns that standard SAST tools miss.

    Supabase RLS Bypass: How Vibe-Coded Apps Get Hacked (2025) — SecuriSky Blog