CWE-284 / Access Control

Supabase & Firebase Row Level Security (RLS) Pitfalls: The Blank Policy That Leaks Everything

How misconfigured PostgreSQL Row Level Security (RLS) and Firebase Security Rules inadvertently expose entire production database tables to public API requests.

💡 Plain English Explainer (ELI5)

In traditional web apps, you write backend code to check if a user is logged in before running an SQL query. In modern platforms like Supabase and Firebase, web browsers talk directly to the database via API. If you create a table and forget to click 'Enable RLS', or write a rule that says `true`, any person on Earth can download your entire database with a single curl command.

Core Concepts & Key Terms

Row Level Security (RLS)
A PostgreSQL feature that evaluates SQL expression filters on every single query to determine whether a row can be viewed or edited.
PostgREST / Direct DB API
The HTTP interface exposed by Supabase that turns client HTTP requests directly into database queries.
Default Open Anti-Pattern
Creating a new database table without explicitly turning on `ENABLE ROW LEVEL SECURITY`, leaving it publicly accessible.
NULL Evaluation Fallacy
Writing policies like `auth.uid() = user_id` without handling NULL conditions, accidentally granting access to unauthenticated requests.

Step-by-Step Attack Flow

Step 1

1. Inspecting Frontend Network Calls

An attacker opens browser DevTools on a production app and copies the public Supabase API URL and anon publishable key.

Step 2

2. Direct REST API Querying

The attacker issues a curl request to /rest/v1/customers?select=* with the anonymous key header.

Step 3

3. RLS Check Evaluation

If RLS was disabled on the customers table, Postgres ignores tenant boundaries and executes a raw table scan.

Step 4

4. Bulk Database Extraction

The complete production table (including hashed passwords, emails, addresses, and private metadata) is returned as a JSON array.

Source Code: Flaw vs. Secure Implementation

VULNERABLE PATTERN
-- VULNERABLE: Table Created Without RLS, Or Flawed Open Policy
CREATE TABLE public.billing_records (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES auth.users(id),
    amount INTEGER NOT NULL,
    credit_card_last4 TEXT NOT NULL
);

-- CRITICAL MISTAKE 1: RLS is NOT enabled! Anyone with anon key has full access!
-- CRITICAL MISTAKE 2: Or an insecure policy like:
ALTER TABLE public.billing_records ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Allow public access" ON public.billing_records
    FOR SELECT
    USING (true); -- Grants EVERYONE read access!
HARDENED DEFENSE
-- SECURE: Explicit RLS Enforcement & Strict Tenant Ownership Policy
CREATE TABLE public.billing_records (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES auth.users(id) NOT NULL,
    amount INTEGER NOT NULL,
    credit_card_last4 TEXT NOT NULL
);

-- 1. Explicitly enable Row Level Security on the table
ALTER TABLE public.billing_records ENABLE ROW LEVEL SECURITY;

-- 2. Force RLS for table owners to avoid accidental superuser bypass
ALTER TABLE public.billing_records FORCE ROW LEVEL SECURITY;

-- 3. Restrict SELECT strictly to verified authenticated user ID
CREATE POLICY "Users can only view their own billing records"
    ON public.billing_records
    FOR SELECT
    TO authenticated
    USING (auth.uid() = user_id);

-- 4. Restrict INSERT to ensure user cannot forge someone else's user_id
CREATE POLICY "Users can only insert their own records"
    ON public.billing_records
    FOR INSERT
    TO authenticated
    WITH CHECK (auth.uid() = user_id);

Engineering Hardening Checklist

← Browse Full Security Directory Explore Reference Blueprints →