Back to Blog
GuideMarch 20265 min read

Your Supabase RLS says 'enabled'. Here's why it's still open.

There is a dangerous misconception in the Supabase ecosystem: developers see the green "RLS Enabled" badge in their dashboard and believe their data is protected. It is not. RLS enabled and RLS secured are two completely different things.

Understanding RLS in Postgres

Row Level Security in PostgreSQL works in two steps. First, you enable it on a table with ALTER TABLE ... ENABLE ROW LEVEL SECURITY. This tells Postgres to check policies before allowing access. Second, you create policies that define who can do what.

Here is the critical detail: when RLS is enabled but no policies exist, access is denied by default. This is actually secure. The problem starts when you add a policy like this:

CREATE POLICY "enable_all" ON my_table
  FOR ALL USING (true) WITH CHECK (true);

This policy says: "For all operations (SELECT, INSERT, UPDATE, DELETE), allow access if true." Since true is always true, this grants unrestricted access to everyone, including anonymous users hitting your API with just the anon key.

Why AI tools generate this pattern

When you prompt an AI to create a table and "enable RLS," it does exactly what you ask. It enables RLS. But the AI also wants your app to work immediately, so it adds a permissive policy to avoid "permission denied" errors during development. The AI does not know whether your table contains private user data or public content. It defaults to making things work.

This is the core tension of vibe coding: the AI optimizes for functionality, not security. The app works, the demo looks great, and nobody realizes the database is wide open until someone exploits it.

What USING(true) actually exposes

With an anon key (which is always exposed in client-side JavaScript) and a USING(true) policy, an attacker can:

  • Read every row in the table using GET /rest/v1/table_name?select=*
  • Insert arbitrary data if the policy includes WITH CHECK(true)
  • Update or delete any row if the policy covers those operations
  • Enumerate your entire schema through the PostgREST API

This is not theoretical. We have seen real apps leaking email addresses, phone numbers, payment information, and private messages because of this exact pattern.

The secure alternative

Every policy should reference auth.uid() to scope access to the authenticated user:

-- Users can only read their own data
CREATE POLICY "read_own" ON my_table
  FOR SELECT USING (auth.uid() = user_id);

-- Users can only insert their own data
CREATE POLICY "insert_own" ON my_table
  FOR INSERT WITH CHECK (auth.uid() = user_id);

-- Users can only update their own data
CREATE POLICY "update_own" ON my_table
  FOR UPDATE USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

For tables with mixed visibility (some public, some private fields), use a Postgres view or a separate public-facing table with limited columns.

Check your policies now

Open the Supabase SQL Editor and run:

SELECT schemaname, tablename, policyname, permissive, cmd, qual
FROM pg_policies
WHERE schemaname = 'public';

If you see qual = true on any row, that table is exposed. Or run a free Sekrd scan and we will check every policy for you automatically.

Don't ship until you're sekrd

Run a free scan to find the vulnerabilities your AI missed.

Scan Your App Free