Supabase RLS is enabled with real policies on every table
Why it matters
A Supabase table without row-level security is readable and writable by anyone who has the anon key — and the anon key ships in every client bundle by design. An attacker who opens devtools on the login page can pull the JWT, hit the REST endpoint directly, and dump or mutate the entire table. Firebase/Supabase misconfiguration incidents catalogued at appsecurity.net show a recurring pattern where RLS is either disabled outright or uses a permissive USING (true) policy, handing every table's contents to anyone with the anon key (which ships in the client bundle). AI coding tools routinely create tables with the Supabase CLI or dashboard without emitting the ENABLE ROW LEVEL SECURITY statement and without writing policies, because the default CREATE TABLE syntax doesn't require it. This is the single most common cause of public Supabase data leaks — the pattern has been behind dozens of disclosed incidents where customer emails, private messages, and payment records were scraped from production anon endpoints.
Severity rationale
Critical because a disabled RLS policy or a `USING (true)` policy exposes every row to unauthenticated reads and writes through the public anon key shipped in every client bundle.
Remediation
For each user table, add ALTER TABLE public.{table} ENABLE ROW LEVEL SECURITY; and define explicit policies. Without RLS, anyone with the anon key can read/write any row.
Deeper remediation guidance and cross-reference coverage for this check lives in the saas-authentication Pro audit — run that after applying this fix for a more exhaustive pass on the same topic.
Detection
- ID:
supabase-rls-enabled-with-real-policies - Severity:
critical - What to look for: Only run if Supabase is detected (via
@supabase/supabase-js,@supabase/ssr, orsupabase/directory). Enumerate every.sqlfile insupabase/migrations/,supabase/schemas/, or any project-level migrations directory. Count occurrences ofDISABLE ROW LEVEL SECURITYandALTER TABLE ... DISABLE ROW LEVEL SECURITY. Then enumerate everyCREATE TABLEstatement and check whether each user-data table (anything not_meta/_audit/migrations) is followed byENABLE ROW LEVEL SECURITY. Then enumerate everyCREATE POLICYstatement and inspect theUSINGandWITH CHECKclauses. A policy whose predicate isUSING (true)orWITH CHECK (true)is categorically NOT a real policy — those predicates mean "no filter, match every row," which is functionally equivalent to no RLS at all when combined with the anon key. The failure patterns you must enumerate:USING (true),WITH CHECK (true),USING (1=1),USING (TRUE)(case-insensitive), and any policy that lacks aUSINGclause entirely (a policy with onlyWITH CHECK (...)filters writes but leaves reads fully open). - Pass criteria: Zero
DISABLE ROW LEVEL SECURITYstatements AND every user-data table has a correspondingENABLE ROW LEVEL SECURITYstatement somewhere in migrations AND everyCREATE POLICYstatement has a non-trivial USING (or WITH CHECK, for insert-only policies) expression that referencesauth.uid(),auth.role(),auth.jwt(), a column comparison (e.g.user_id = auth.uid(),tenant_id = current_setting('...')), or a JOIN-derived predicate (e.g.EXISTS (SELECT 1 FROM memberships WHERE ...)).USING (true)andWITH CHECK (true)are categorically NOT real policies and do NOT count — no exceptions, unless the in-code waiver below is satisfied. - In-code waiver: A
USING (true)policy is acceptable if and ONLY if the immediately preceding line in the same.sqlfile is a comment matching^-- INTENTIONALLY_PUBLIC:followed by at least one word explaining why the table is intentionally world-readable (e.g.-- INTENTIONALLY_PUBLIC: public marketing specials listing, no PII). The waiver must be on the line directly before theCREATE POLICYstatement; comments on the table definition or on other lines do not count. When the waiver is present, the policy passes for that specific table. Count waived policies separately in the pass report. - Fail criteria: At least one
DISABLE ROW LEVEL SECURITYstatement, OR at least one user-dataCREATE TABLEwithout a matchingENABLE ROW LEVEL SECURITY, OR any policy whose only predicate isUSING (true)/WITH CHECK (true)/USING (1=1)without the-- INTENTIONALLY_PUBLIC:waiver on the preceding line. - Skip (N/A) when: Supabase is not detected. Quote the absence:
"Supabase not detected: no @supabase/* dependency or supabase/ directory". - Do NOT pass when: RLS is enabled in dev but commented out in prod migrations. A
USING (true)policy does NOT count as a real policy — it matches every row and hands the table to anyone with the anon key. A policy that wrapstruein extra syntax (e.g.USING ((true)),USING (TRUE),USING (1=1)) is the same failure in disguise and must also be flagged. A project-wide "lock everything down later" comment is not a waiver — the waiver must be-- INTENTIONALLY_PUBLIC:on the line directly before the specific permissive policy. - Before evaluating, quote: Quote the full text of each
CREATE POLICYstatement (or the first 120 characters) so the USING/WITH CHECK predicate is visible. Quote the first 80 characters of anyCREATE TABLEstatement that lacks a pairedENABLE ROW LEVEL SECURITY. For anyUSING (true)policy, quote the line immediately preceding it to confirm whether the-- INTENTIONALLY_PUBLIC:waiver is present. - Report even on pass:
"Found N user tables in migrations; all N have ENABLE ROW LEVEL SECURITY. M policies total: K with real predicates (auth.uid / column comparison / JOIN), L with INTENTIONALLY_PUBLIC waiver, 0 with unwaived USING (true)." - Detail on fail:
"public.specials table has USING (true) policy without INTENTIONALLY_PUBLIC waiver (20260101_init.sql:47)"or"Tableprofilescreated in 20260101_init.sql but no ENABLE ROW LEVEL SECURITY found"or"3 user tables with unwaived USING (true) policies: profiles, posts, messages". - Cross-reference: For full auth coverage (signup, login, password-reset, MFA, session lifecycle), run the
saas-authenticationaudit. - Remediation: For each user table, add
ALTER TABLE public.{table} ENABLE ROW LEVEL SECURITY;and define explicit policies. Without RLS, anyone with the anon key can read/write any row.
Taxons
History
- 2026-04-22·v1.0.0·Initial import from project-snapshot via Phase 8.1 bundling·by phase-8-1-bundle-project-snapshot
- 2026-04-22·v2.0.0·Phase 9 consequence-first restructure — moved to new section slug, added news/incident references, severity reviewed.·by phase-9-stack-scan-v3
- 2026-04-23·v2.1.0·Phase 9.1 tighten — `USING (true)` / `WITH CHECK (true)` categorically disqualified as real policies; explicit in-code `-- INTENTIONALLY_PUBLIC:` waiver added for legitimate public tables.·by phase-9-1-stack-scan-v3-1