Every table must have Row Level Security enabled with at least one policy per operation. Tables without RLS are accessible to any authenticated user. [CWE-862 · A01:2021]
Impact: CRITICAL (prevents unauthorized data access across tenants)
Every table in a Supabase project must have Row Level Security (RLS) enabled. Without RLS, any authenticated user (or anyone with the anon key) can read, insert, update, and delete all rows in the table. This is the most dangerous security misconfiguration in Supabase applications — it turns every table into a public API.
Even with RLS enabled, you need at least one policy per operation type (SELECT, INSERT, UPDATE, DELETE) or access is silently denied for that operation.
Incorrect (table without RLS — fully exposed):
-- ❌ No RLS — any authenticated user can read/write ALL rowsCREATE TABLE public.documents ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid REFERENCES auth.users(id) NOT NULL, title text NOT NULL, content text, created_at timestamptz DEFAULT now());-- Forgot ALTER TABLE ... ENABLE ROW LEVEL SECURITY-- Forgot to create any policies-- Result: SELECT * FROM documents returns ALL documents for ALL users
Correct (RLS enabled with policies for all operations):
-- ✅ Table with RLS enabled and complete policiesCREATE TABLE public.documents ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid REFERENCES auth.users(id) NOT NULL, title text NOT NULL, content text, created_at timestamptz DEFAULT now());-- Step 1: Enable RLSALTER TABLE public.documents ENABLE ROW LEVEL SECURITY;-- Step 2: SELECT — users can only read their own documentsCREATE POLICY "Users can view own documents" ON public.documents FOR SELECT USING (auth.uid() = user_id);-- Step 3: INSERT — users can only insert with their own user_idCREATE POLICY "Users can create own documents" ON public.documents FOR INSERT WITH CHECK (auth.uid() = user_id);-- Step 4: UPDATE — users can only update their own documentsCREATE POLICY "Users can update own documents" ON public.documents FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);-- Step 5: DELETE — users can only delete their own documentsCREATE POLICY "Users can delete own documents" ON public.documents FOR DELETE USING (auth.uid() = user_id);
RLS checklist for every migration:
[ ] ALTER TABLE ... ENABLE ROW LEVEL SECURITY[ ] SELECT policy with USING clause[ ] INSERT policy with WITH CHECK clause[ ] UPDATE policy with both USING and WITH CHECK clauses[ ] DELETE policy with USING clause[ ] Index on the column used in policies (e.g., user_id)[ ] Test: can user A read user B's rows? (should fail)[ ] Test: can anon key read any rows? (should fail unless intended)
Relational RLS (access through parent table):
-- For child tables, reference the parent's ownershipCREATE POLICY "Users can view own document comments" ON public.comments FOR SELECT USING ( EXISTS ( SELECT 1 FROM public.documents WHERE documents.id = comments.document_id AND documents.user_id = auth.uid() ) );
Detection hints:
# Find CREATE TABLE statements without corresponding RLSgrep -rn "CREATE TABLE" supabase/migrations/ --include="*.sql"# Check if ENABLE ROW LEVEL SECURITY follows each CREATE TABLEgrep -rn "ENABLE ROW LEVEL SECURITY" supabase/migrations/ --include="*.sql"