DEV Community

Socuv
Socuv

Posted on

Supabase RLS: The Hidden Danger (And How to Find It Before Hackers Do)

You just launched your Supabase project. It works. Users are signing up. You're proud of it.

Then you get a message: "Hey, I can see everyone's data."

This happens more than you'd think. And the cause is almost always the same: Row Level Security was enabled, but the policies were wrong — or missing entirely.

Let me show you exactly how this happens, how to check if your project is affected, and how to fix it.


What Is RLS and Why Does It Matter?

Supabase uses PostgreSQL's Row Level Security to control which rows a user can read, insert, update, or delete. When you enable it, access is denied by default — until you create policies that explicitly allow access.

The problem: enabling RLS and creating correct policies are two separate steps. You can do one without the other.

And Supabase's dashboard will show your table as "RLS enabled" — technically true, but completely misleading if you have no policies.


The Most Common Mistake: Enabling RLS Without Policies

Here's what a dangerous table looks like:

-- RLS is enabled
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;

-- But no policies exist
-- Result: NO rows are returned for authenticated users
-- BUT: anon users can still read everything if you have a permissive grant
Enter fullscreen mode Exit fullscreen mode

Or worse — the AI-generated version that "works" in testing:

-- The classic AI-generated mistake
CREATE POLICY "Enable read access for all users"
ON user_profiles FOR SELECT
USING (true);  -- This allows EVERYONE to read EVERYTHING
Enter fullscreen mode Exit fullscreen mode

I've seen this pattern in dozens of projects. Cursor, Lovable, and Bolt generate it because it makes tests pass. It's not secure.


What an Attacker Sees

Your frontend bundles your Supabase URL and anon key. This is normal — it's designed this way. The anon key is meant to be public.

The problem is what that key can access.

Here's how easy it is to check:

# Anyone can run this with your public anon key
curl 'https://YOUR_PROJECT.supabase.co/rest/v1/user_profiles?select=*' \
  -H "apikey: YOUR_ANON_KEY" \
  -H "Authorization: Bearer YOUR_ANON_KEY"
Enter fullscreen mode Exit fullscreen mode

If your RLS policies are wrong, this returns all user data. Email addresses, names, profile information — everything in that table.

No authentication required. Just your public anon key, which is already in your JavaScript bundle.


The Four RLS Failure Modes

After analyzing many Supabase projects, these are the patterns I see most often:

1. Missing policies on new tables

You add a new table in a hurry. You enable RLS (or forget to). No policy. The table is either locked (nothing works) or wide open (everything is readable).

2. USING (true) policies

The "it works now" shortcut. Every row is readable by every user. Often copy-pasted from documentation examples that were never meant for production.

3. Permissive storage buckets

-- This bucket is publicly readable
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true);
Enter fullscreen mode Exit fullscreen mode

If you store anything sensitive in a "public" bucket — even with a non-obvious path — it's accessible to anyone with the URL.

4. Overpermissive service role usage

Some developers use the service_role key in their frontend for "simplicity." The service role bypasses RLS entirely. It should never be in client-side code.


How to Check Your Project Right Now

Step 1: Check your RLS status

-- Run this in Supabase SQL Editor
SELECT
  schemaname,
  tablename,
  rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
Enter fullscreen mode Exit fullscreen mode

Any table where rowsecurity = false is unprotected.

Step 2: Check your policies

-- See all policies on public tables
SELECT
  schemaname,
  tablename,
  policyname,
  permissive,
  roles,
  cmd,
  qual,
  with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;
Enter fullscreen mode Exit fullscreen mode

Look for:

  • Tables with no policies at all (but RLS enabled)
  • Policies with qual = '(true)' — these allow everyone
  • Policies that don't check auth.uid()

Step 3: Check your storage buckets

SELECT id, name, public
FROM storage.buckets;
Enter fullscreen mode Exit fullscreen mode

If public = true, anyone can list and access files in that bucket.

Step 4: Check your auth settings

In your Supabase dashboard → Authentication → Settings:

  • Is email confirmation required? (it should be for production)
  • Is there a password minimum length? (8+ characters)
  • Are there rate limits on sign-ups?

The Correct Pattern

Here's what secure RLS looks like:

-- Users can only read their own profile
CREATE POLICY "Users can read own profile"
ON user_profiles FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

-- Users can only update their own profile
CREATE POLICY "Users can update own profile"
ON user_profiles FOR UPDATE
TO authenticated
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);

-- No insert from client (handle in Edge Function)
-- No delete from client (handle in Edge Function)
Enter fullscreen mode Exit fullscreen mode

The key parts:

  • TO authenticated — only logged-in users
  • auth.uid() = user_id — only their own rows
  • Separate policies for SELECT, INSERT, UPDATE, DELETE — don't use a blanket policy

For Vibe Coders Specifically

If you built your project with Cursor, Lovable, Bolt, or v0 — check your RLS policies manually. These tools are excellent at building features quickly, but they optimize for "it works" over "it's secure."

The generated code often includes USING (true) policies because they're needed to make the demo work in development. Before you go live, replace them with proper user-scoped policies.

This is not a criticism of AI coding tools — it's just how they work. Security is your responsibility, not the tool's.


Automating This Check

Manually auditing your Supabase project is doable but tedious. If you want to automate it — especially if you manage multiple projects — there are a few tools:

  • SupaSec (open source): bundle scraping + basic RLS check
  • FounderScan ($19 one-time): 12+ security checks including Supabase
  • AEGIS (my project): continuous monitoring, active testing, plain-language report with fix commands

The important thing isn't which tool you use — it's that you check at all. Most projects I've looked at have at least one issue. Many have several.


TL;DR

  1. Enabling RLS ≠ being secure. You need policies too.
  2. USING (true) is almost never what you want in production.
  3. Public storage buckets are public. Don't store sensitive data in them.
  4. The service_role key bypasses RLS. It belongs on the server, not in the browser.
  5. Run the SQL queries above to check your project right now. It takes 5 minutes.

If you find something, fix it before someone else does.


I'm building AEGIS — automated Supabase security scanning with continuous monitoring. If you want to be notified when it launches, join the waitlist.

Top comments (0)