Supabase: When Row-Level Security Isn't Enough
Row-Level Security is Supabase's primary access control mechanism. But RLS only protects PostgREST queries. It doesn't cover service_role keys hardcoded in client bundles, anon key abuse through realtime channels, or storage bucket ACL misconfigurations that lead to data breaches. A penetration testing walkthrough for Supabase security audits.
The default trust model
Supabase projects ship with two API keys: an anon key (public, embedded in your frontend) and a service_role key (secret, full database access). Row-Level Security policies only gate requests made through PostgREST with the anon key. If the service_role key leaks, RLS is irrelevant.
Where service_role keys leak
The most common leak vector is client-side JavaScript bundles. Developers set NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY thinking the NEXT_PUBLIC_ prefix just makes it available in the browser, not realizing it literally embeds the key in the shipped JavaScript.
We've also found service_role keys in:
- Git history. .env committed once, then .gitignored (the key is still in the reflog)
- Error tracking. Sentry breadcrumbs capturing the full Supabase client initialization
- Realtime channels. The WebSocket handshake includes the key in the URL params
The Realtime bypass
Even with a properly scoped anon key, Supabase Realtime channels can leak data that RLS should block. If a table has Realtime enabled and the RLS policy only restricts SELECT, an attacker can subscribe to INSERT and UPDATE events on the channel and receive every row change in real time. bypassing the SELECT restriction entirely.
Storage bucket ACLs
Supabase Storage has its own access control system separate from database RLS. A common misconfiguration: the database tables are locked down with proper RLS, but the storage buckets are set to public. Uploaded files (user avatars, documents, exports) are accessible to anyone with the URL.
How we detect this
Our scanner:
- Extracts both keys from the client JavaScript bundle
- Tests the service_role key against the PostgREST API
- Checks Realtime channel subscriptions with the anon key
- Enumerates storage buckets and tests anonymous access
- Verifies that RLS policies exist on every public-facing table
Defense
- Never prefix your service_role key with
NEXT_PUBLIC_orVITE_ - Audit git history:
git log --all -p. '.env*' | grep service_role - Disable Realtime on tables that don't need it
- Set storage buckets to private and use signed URLs
- Use
supabase db lintto check for tables without RLS
Want us to check your Supabase setup?
Our scanner detects this exact misconfiguration. plus dozens more across 38 platforms. Free website check available, no commitment required.
