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
);
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);
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');
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
);
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!
);
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
);
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');
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]
);
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
The Fix
- Enable email confirmation in Supabase Dashboard under Authentication > Settings
- 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
);
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"
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
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;
$$;
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;
$$;
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');
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 });
}
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))
})
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))
})
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
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)