Vulnerability
Supabase

Broken Authentication in Supabase Apps

Supabase provides built-in auth with Supabase Auth, but broken authentication occurs when RLS policies do not properly check auth.uid(), when client-side auth state is trusted without server verification, and when service_role keys are exposed in frontend code.

Scan Your Supabase App

How Broken Authentication Manifests in Supabase

Broken auth in Supabase apps typically manifests as: RLS policies that check auth.uid() but do not verify the user's role or subscription status, allowing any authenticated user to access premium or admin-only data. Trusting the client-side auth state (supabase.auth.getUser()) without verifying the JWT server-side. The client can be manipulated to return arbitrary user data. Using the service_role key in frontend code, which bypasses RLS entirely and gives full database access to anyone who finds the key in the JavaScript bundle. Forgetting to enable RLS on new tables, leaving them accessible to any request with the public anon key.

Real-World Impact

A Supabase SaaS app checked subscription status client-side and displayed premium features accordingly. However, the RLS policies only checked auth.uid() for ownership, not subscription tier. Free users discovered they could directly query premium data tables using the Supabase client, accessing features and data they had not paid for.

Step-by-Step Fix

1

Write proper RLS policies

Include auth checks and role verification in RLS policies.

-- Enable RLS
ALTER TABLE "documents" ENABLE ROW LEVEL SECURITY;

-- Basic ownership check
CREATE POLICY "Users can view own documents" ON "documents"
  FOR SELECT TO authenticated
  USING ((select auth.uid()) = user_id);

-- Role-based access
CREATE POLICY "Admins can view all documents" ON "documents"
  FOR SELECT TO authenticated
  USING (
    (select auth.jwt() ->> 'role') = 'admin'
  );

-- Subscription-aware policy
CREATE POLICY "Premium users access premium content" ON "premium_content"
  FOR SELECT TO authenticated
  USING (
    EXISTS (
      SELECT 1 FROM subscriptions
      WHERE user_id = (select auth.uid())
      AND status = 'active'
      AND plan = 'premium'
    )
  );
2

Verify auth in Edge Functions

Always validate the JWT server-side in Edge Functions.

import { createClient } from '@supabase/supabase-js';

Deno.serve(async (req) => {
  const authHeader = req.headers.get('Authorization');
  if (!authHeader) {
    return new Response('Unauthorized', { status: 401 });
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: authHeader } } }
  );

  const { data: { user }, error } = await supabase.auth.getUser();
  if (error || !user) {
    return new Response('Unauthorized', { status: 401 });
  }

  // Now user is verified server-side
});
3

Check for exposed service_role key

Ensure the service_role key is never used in frontend code.

// UNSAFE - service_role in frontend
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // Bypasses all RLS!
);

// SAFE - anon key in frontend
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! // RLS enforced
);

// service_role only in server-side code (API routes, Edge Functions)
// Never prefix with NEXT_PUBLIC_

Prevention Best Practices

1. Always use (select auth.uid()) in RLS policies, not just auth.uid(). 2. Enable RLS on every table immediately after creation. 3. Never use the service_role key in frontend code. 4. Verify auth server-side in Edge Functions and API routes. 5. Include role/subscription checks in RLS policies, not just ownership.

How to Test

1. Try accessing tables directly with just the anon key (no auth). 2. Check if RLS is enabled: SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'; 3. Search frontend code for service_role or SUPABASE_SERVICE_ROLE. 4. Test role-based access by querying as a user with insufficient permissions. 5. Use Vibe App Scanner to detect broken auth in your Supabase application.

Frequently Asked Questions

Is the Supabase anon key a security risk?

No. The anon key is designed to be public. Security is enforced by Row Level Security (RLS) policies on each table. The anon key allows the client to communicate with PostgREST, but RLS determines what data the user can access based on their JWT.

Does RLS replace application-level auth checks?

RLS is a critical security layer but should complement, not replace, application-level checks. Verify auth in your API routes and Edge Functions as well. RLS is the last line of defense at the database level.

How do I handle role-based access in Supabase?

Store roles in a user_roles table or as custom claims in the JWT. Use auth.jwt() in RLS policies to check role claims. For complex role hierarchies, create a PostgreSQL function that checks permissions and call it from RLS policies.

Is Your Supabase App Vulnerable to Broken Authentication?

VAS automatically scans for broken authentication vulnerabilities in Supabase applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Supabase stack.