Skip to main content

Supabase Row-Level Security is not disabled

ab-002569 · project-snapshot.auth-access.no-supabase-rls-disabled
Severity: criticalactive

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. 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 or missing RLS policy exposes every row in the table to unauthenticated reads and writes through the public anon key, with no code change required to exploit it.

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: project-snapshot.auth-access.no-supabase-rls-disabled
  • Severity: critical
  • What to look for: Only run if Supabase is detected (via @supabase/supabase-js, @supabase/ssr, or supabase/ directory). Enumerate every .sql file in supabase/migrations/, supabase/schemas/, or any project-level migrations directory. Count occurrences of DISABLE ROW LEVEL SECURITY and ALTER TABLE ... DISABLE ROW LEVEL SECURITY. Then enumerate every CREATE TABLE statement and check whether each user-data table (anything not _meta/_audit/migrations) is followed by ENABLE ROW LEVEL SECURITY.
  • Pass criteria: Zero DISABLE ROW LEVEL SECURITY statements AND every user-data table has a corresponding ENABLE ROW LEVEL SECURITY statement somewhere in migrations.
  • Fail criteria: At least one DISABLE ROW LEVEL SECURITY statement, or at least one user-data CREATE TABLE without a matching ENABLE ROW LEVEL SECURITY.
  • 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, or when policies exist but are USING (true) (which is effectively disabled).
  • Before evaluating, quote: Quote the first 80 characters of any CREATE TABLE statement that lacks a paired ENABLE ROW LEVEL SECURITY.
  • Report even on pass: "Found N user tables in migrations; all N have ENABLE ROW LEVEL SECURITY."
  • Detail on fail: "Table profiles created in 20260101_init.sql but no ENABLE ROW LEVEL SECURITY found" or "3 user tables without RLS: profiles, posts, messages".
  • Cross-reference: For full auth coverage (signup, login, password-reset, MFA, session lifecycle), run the saas-authentication audit.
  • 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