DEV Community

Cover image for 10 Common Supabase Security Misconfigurations (and How to Fix Them)
Victor Yrazusta Ibarra
Victor Yrazusta Ibarra

Posted on • Originally published at modernpentest.com

10 Common Supabase Security Misconfigurations (and How to Fix Them)

Supabase has become one of the most popular backend-as-a-service platforms for modern web applications. Its PostgreSQL foundation, real-time capabilities, and developer-friendly APIs make it an excellent choice for startups and established companies alike.

However, with great power comes great responsibility. Supabase's flexibility means there are many ways to misconfigure your security settings, potentially exposing sensitive user data. In this guide, we'll cover the 10 most common Supabase security misconfigurations we've seen in production applications.

1. Missing Row-Level Security (RLS)

Severity: Critical

The most dangerous misconfiguration is having no Row-Level Security (RLS) enabled at all. When RLS is disabled, anyone with your Supabase URL and anon key (which is public) can read, update, or delete all data in your tables.

The Problem

-- This table has NO RLS - anyone can access all data
create table user_profiles (
  id uuid references auth.users primary key,
  email text,
  full_name text,
  credit_card_last_four text
);
Enter fullscreen mode Exit fullscreen mode

The Fix

Always enable RLS and create appropriate policies:

-- Enable RLS
alter table user_profiles enable row level security;

-- Allow users to read only their own profile
create policy "Users can read own profile"
  on user_profiles for select
  using (auth.uid() = id);

-- Allow users to update only their own profile
create policy "Users can update own profile"
  on user_profiles for update
  using (auth.uid() = id);
Enter fullscreen mode Exit fullscreen mode

2. Overly Permissive RLS Policies

Severity: High

Even with RLS enabled, policies that are too broad can expose data. A common mistake is using true or weak conditions.

The Problem

-- This policy allows ANY authenticated user to read ALL profiles
create policy "Authenticated users can read profiles"
  on user_profiles for select
  using (auth.role() = 'authenticated');
Enter fullscreen mode Exit fullscreen mode

The Fix

Be specific about what data each user can access:

-- Only allow reading your own profile OR public profiles
create policy "Users can read own or public profiles"
  on user_profiles for select
  using (
    auth.uid() = id
    OR is_public = true
  );
Enter fullscreen mode Exit fullscreen mode

3. Service Role Key Exposure

Severity: Critical

The service_role key bypasses all RLS policies and should NEVER be exposed to the client. We've seen this key hardcoded in frontend applications, environment variables leaked in client bundles, and even committed to public repositories.

The Problem

// NEVER do this - service_role key in frontend code
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY // DANGER!
);
Enter fullscreen mode Exit fullscreen mode

The Fix

Only use the anon key in client-side code:

// Correct - using anon key which respects RLS
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);
Enter fullscreen mode Exit fullscreen mode

Keep service_role key only in server-side code (API routes, server actions, edge functions).

4. Insecure Storage Bucket Policies

Severity: High

Supabase Storage uses similar RLS-style policies, but many developers forget to configure them, leaving files publicly accessible.

The Problem

-- Default policy allows public access to all files
create policy "Public access"
  on storage.objects for select
  using (bucket_id = 'avatars');
Enter fullscreen mode Exit fullscreen mode

The Fix

Restrict access based on ownership:

-- Users can only access their own files
create policy "User can access own files"
  on storage.objects for select
  using (
    bucket_id = 'user-files'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- Users can upload to their own folder
create policy "User can upload own files"
  on storage.objects for insert
  with check (
    bucket_id = 'user-files'
    AND auth.uid()::text = (storage.foldername(name))[1]
  );
Enter fullscreen mode Exit fullscreen mode

5. Missing Email Verification

Severity: Medium

By default, Supabase allows users to sign up and immediately access the application without verifying their email. This enables account enumeration and fake account creation.

The Problem

// User can sign up and immediately access protected resources
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password123'
});
// data.user exists and can be used immediately
Enter fullscreen mode Exit fullscreen mode

The Fix

  1. Enable email confirmation in Supabase Dashboard under Authentication > Settings
  2. Check email confirmation status in your RLS policies:
create policy "Only verified users can access data"
  on sensitive_data for select
  using (
    auth.uid() = user_id
    AND auth.jwt()->>'email_confirmed_at' is not null
  );
Enter fullscreen mode Exit fullscreen mode

6. Direct Table Access via PostgREST

Severity: Medium

Supabase exposes your database through PostgREST, which means any table can be queried directly if not properly secured. Developers sometimes create tables without RLS, thinking they're "internal only."

The Problem

# Anyone can query this table directly via the REST API
curl 'https://your-project.supabase.co/rest/v1/internal_logs' \
  -H "apikey: YOUR_ANON_KEY"
Enter fullscreen mode Exit fullscreen mode

The Fix

Always enable RLS on all tables, even "internal" ones:

-- Secure internal tables by denying all access via RLS
alter table internal_logs enable row level security;

-- No policies = no access via PostgREST
-- Access only through server-side code with service_role
Enter fullscreen mode Exit fullscreen mode

7. Exposed Database Functions

Severity: High

PostgreSQL functions created with SECURITY DEFINER run with the privileges of the function creator, not the calling user. If exposed via PostgREST without proper checks, they can bypass RLS.

The Problem

-- This function runs with elevated privileges
create or replace function delete_user(target_id uuid)
returns void
language sql
security definer
as $$
  delete from user_profiles where id = target_id;
$$;
Enter fullscreen mode Exit fullscreen mode

The Fix

Add authorization checks inside the function:

create or replace function delete_user(target_id uuid)
returns void
language plpgsql
security definer
as $$
begin
  -- Check if the calling user is authorized
  if auth.uid() != target_id then
    raise exception 'Not authorized';
  end if;

  delete from user_profiles where id = target_id;
end;
$$;
Enter fullscreen mode Exit fullscreen mode

8. Weak Password Requirements

Severity: Medium

Supabase's default minimum password length is 6 characters, which is quite weak by modern standards.

The Fix

Configure stronger password requirements in your Supabase Dashboard under Authentication > Settings:

  • Minimum length: 12 characters
  • Require uppercase, lowercase, numbers, and symbols

Additionally, implement client-side validation:

const passwordSchema = z.string()
  .min(12, 'Password must be at least 12 characters')
  .regex(/[A-Z]/, 'Password must contain uppercase letter')
  .regex(/[a-z]/, 'Password must contain lowercase letter')
  .regex(/[0-9]/, 'Password must contain a number')
  .regex(/[^A-Za-z0-9]/, 'Password must contain special character');
Enter fullscreen mode Exit fullscreen mode

9. Missing Rate Limiting

Severity: Medium

Supabase doesn't have built-in rate limiting for authentication endpoints, making your app vulnerable to brute force attacks.

The Fix

Implement rate limiting at the application level:

// Using Upstash Redis for rate limiting
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 attempts per minute
});

export async function signIn(email: string, password: string) {
  const { success } = await ratelimit.limit(email);

  if (!success) {
    throw new Error('Too many login attempts. Please try again later.');
  }

  return supabase.auth.signInWithPassword({ email, password });
}
Enter fullscreen mode Exit fullscreen mode

10. Misconfigured Edge Functions

Severity: High

Supabase Edge Functions are powerful serverless functions that run on Deno. A common mistake is deploying functions without proper JWT verification, making them accessible to anyone who knows the endpoint URL.

The Problem

// This edge function has NO authentication check
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'

serve(async (req) => {
  // DANGER: Anyone can call this endpoint!
  const { userId } = await req.json()

  // Performing privileged operations without auth
  const userData = await adminClient
    .from('users')
    .select('*')
    .eq('id', userId)
    .single()

  return new Response(JSON.stringify(userData))
})
Enter fullscreen mode Exit fullscreen mode

The Fix

Always verify the JWT token and extract the user from it:

import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  // Extract the JWT from the Authorization header
  const authHeader = req.headers.get('Authorization')
  if (!authHeader) {
    return new Response('Missing authorization header', { status: 401 })
  }

  // Create a Supabase client with the user's JWT
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    { global: { headers: { Authorization: authHeader } } }
  )

  // Verify the token and get the user
  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    return new Response('Invalid token', { status: 401 })
  }

  // Now safely use user.id instead of trusting client input
  const { data } = await supabase
    .from('users')
    .select('*')
    .eq('id', user.id)
    .single()

  return new Response(JSON.stringify(data))
})
Enter fullscreen mode Exit fullscreen mode

Detecting These Issues Automatically

Manually checking all tables, policies, and configurations is tedious and error-prone. We built Supabomb, an open source CLI tool specifically for Supabase security testing.

# Install and run
git clone https://github.com/Victoratus/supabomb.git
cd supabomb

# Discover credentials from your app
uv run supabomb discover --url https://your-app.com

# Run security tests
uv run supabomb test
Enter fullscreen mode Exit fullscreen mode

Supabomb automatically:

  • Discovers Supabase credentials from your frontend
  • Enumerates all accessible tables and storage buckets
  • Tests RLS policies and identifies misconfigurations
  • Compares anonymous vs. authenticated access
  • Generates detailed findings reports

Read more about it in our Introducing Supabomb post.

Conclusion

Securing your Supabase application requires a defense-in-depth approach. Here's a checklist to review:

  • [ ] RLS is enabled on ALL tables
  • [ ] RLS policies are specific and well-tested
  • [ ] Service role key is only used server-side
  • [ ] Storage bucket policies are configured
  • [ ] Email verification is required
  • [ ] All database functions have proper authorization checks
  • [ ] Password requirements are strong
  • [ ] Rate limiting is implemented
  • [ ] Edge Functions verify JWT tokens before processing requests

Automate Your Supabase Security

Want continuous protection instead of manual checks? ModernPentest's Supabase Security Scanning uses AI agents trained specifically on Supabase vulnerabilities to:

  • Scan in under an hour - Full penetration test + SOC 2 report
  • Catch RLS bypasses - The same issues covered in this guide, detected automatically
  • Monitor continuously - Weekly/daily scans catch regressions before attackers do
  • Generate compliance reports - Auditor-ready documentation for SOC 2, ISO 27001

Our agents use Supabomb under the hood, combined with AI analysis to prioritize findings by actual risk and provide specific remediation guidance.

Start Your Free Security Scan →


This guide is part of our Security Guides series. Follow us for more content on securing modern web applications.

Top comments (0)