Your Supabase RLS is Leaking. You Just Don't Know It Yet.
I have spent the last six years cleaning up after developers who trusted marketing copy a little too much. Don't get me wrong. I love Supabase. It makes spinning up a backend feel like magic. But that magic has a dark side, and it lives in your database policies.
When you build with Supabase, the marketing promises you a frontend-only paradise where you do not need to write a traditional backend, but what they do not tell you is that by skipping the backend, you are moving the entire security perimeter of your company into a set of SQL statements written in a tiny text box in a web dashboard.
Most developers click the little green "Enable RLS" toggle on a table, write a policy that looks roughly like auth.uid() = user_id, run a couple of local integration tests, and call it a day. They assume they are safe.
They are usually wrong.
The 41,200 Manifest Leak
Last October, I got a frantic call from a logistics startup. They had just raised a seed round and were preparing for an enterprise security audit. They wanted a quick sanity check on their database. They were confident. "We have 100% RLS coverage," the lead dev told me. "Every single table has Row Level Security enabled."
I booted up my environment, bypassed their client-side SDK, and targeted their endpoints directly using a standard HTTP client. Within twenty minutes, I downloaded 41,200 shipping manifests containing real home addresses, phone numbers, and delivery instructions. I did not use an admin key. I did not hack their servers. I just logged in as a newly registered, free-tier user and asked the database for the data.
How?
They had a helper function that checked whether a user belonged to an organization. It was defined as SECURITY DEFINER. This is a common Postgres feature that runs the function with the privileges of the user who created it (usually the admin), rather than the user running it. Because of a slight logic flaw in their nested OR statement, the function returned true if the organization ID parameter was passed as null. Since it ran as admin, it bypassed all standard checks. The database happily handed over every single row in the system.
It looked clean. It passed their local Jest tests because their tests only checked happy paths using the service role key. But the real world does not use the service role key.
The Three Silent Killers of Supabase Security
If you are running a production app on Supabase right now, there is a high probability you have at least one of these three vulnerabilities in your schema.
1. The "Unintended Public" Join Table
You remember to enable RLS on your users table and your organizations table. But what about that many-to-many join table you created in a hurry last Tuesday? You know, the workspace_members table. If you forget to enable RLS on a join table, Postgres allows anyone to read it by default. An attacker might not be able to read the private data in the organizations table directly, but they can easily reconstruct your entire user directory and company structure just by querying the unprotected join table.
2. Relying on auth.uid() Without Checking the Role
Writing auth.uid() = user_id is fine until you realize that anonymous users also have access to certain endpoints. If your policy does not explicitly check if the request is authenticated (using auth.role() = 'authenticated'), a clever attacker can spoof requests or exploit null states to bypass your identity checks entirely.
3. Performance-Induced Bypasses
Nested queries inside RLS policies run for every single row returned by a query. If you have a query that returns 100 rows, and your RLS policy has a subquery that checks workspace membership, Postgres is executing that subquery 100 times. When the app starts slowing down, developers often "optimize" policies by removing checks or using loose caching functions that inadvertently leak data across tenant boundaries.
How to Actually Test Your Policies
Stop testing your security from your frontend code. Your frontend is untrusted territory. If you want to know if your policies actually work, you need to test them directly in the database as a restricted user.
Open your Supabase SQL editor and stop running queries as the default superuser. Instead, emulate a real client session. Run this block before you test your queries:
-- Step 1: Switch to the authenticated role
SET ROLE authenticated;
-- Step 2: Set the claim for the user you want to test
SET LOCAL request.jwt.claims = '{"sub": "your-test-user-uuid-here"}';
-- Step 3: Run your query and see what actually comes back
SELECT * FROM secure_table;
If you see rows that do not belong to that UUID, your security is broken. Period.
Configuring Row Level Security Supabase Postgres setups requires a deep understanding of database sessions, execution contexts, and policy performance. It is not something you can just set and forget.
Get a Professional to Check Your Code
If you are building something that handles real customer data, financial records, or private health information, you cannot afford to guess. I run GuardLabs. We are a specialized team that does not write generic blog posts or sell overpriced enterprise software. We do one thing: we find the silent leaks in your database before your customers (or hackers) do. If you want a hands-on, deep-dive audit of your policies, check out our service: Аудит и настройка Row Level Security в Supabase/Postgres. We will find your leaks, write clean SQL fixes, and give you the peace of mind that your database is actually locked down. If you need a dedicated supabase rls audit freelance expert to look over your schema, we are ready to jump in.
Top comments (0)