Home/Blog/Vulnerability Research
Vulnerability ResearchPublished · Updated ⚡ 15 min read

Stop shipping broken RLS policies to prod.

A deep dive into real-world RLS bypass techniques, Supabase auth misconfigurations, and how to stop leaking your production database to the internet.

VG
Vladyslav Gusarov
DevSecOps Lead at Bryxe
Supabase Security: Row Level Security Mistakes That Will Get You Hacked [Audit]

Look. I'm tired of seeing the same lazy mistakes.

Every week, some startup ships a "secure" app on Supabase. Three days later, they get dumped on a hacking forum. Why? Because developers treat Row Level Security (RLS) like it's a magic wand. It's not. It's a loaded gun pointed directly at your foot.

Supabase is phenomenal under the hood. Postgres is a beast. But when you glue them together with janky authentication assumptions, you're asking for a bad time. You enable RLS, copy-paste a snippet from StackOverflow, and call it a day. Then you push to prod.

Newsflash: Your data is already leaking.

Honestly, it's a nightmare. Let's tear apart the absolute worst Supabase RLS security blunders I've seen in the wild this year. We're talking real PostgreSQL RLS bypass techniques that attackers actively exploit. Grab some coffee. We're going deep.

The 'Authenticated Role' Trap

This is the big one. The ultimate footgun.

You create a table. You enable RLS. You add a policy:

sqlSource Code
CREATE POLICY "Users can view own data"
ON user_profiles FOR SELECT
TO authenticated
USING (true);

See the problem? I see it instantly.

TO authenticated just means the user has a valid JWT from Supabase Auth. Any user. If I register on your site, I am authenticated. With that policy, I can now read the entire user_profiles table. You just built a public API directory for your competitors to scrape.

Honestly, it's a nightmare. The secure way? Check the damn UUID.

sqlSource Code
CREATE POLICY "Users can view own data"
ON user_profiles FOR SELECT
TO authenticated
USING (auth.uid() = id);

It takes two extra seconds to write. Do it.

The Security Definer Disaster

People love writing Postgres functions (RPCs) in Supabase. It's easy. But then they slap SECURITY DEFINER on it because they ran into permissions errors and couldn't be bothered to fix them properly.

SECURITY DEFINER executes the function with the privileges of the user that created it. Usually, that's the Postgres superuser.

sqlSource Code
CREATE OR REPLACE FUNCTION delete_workspace(workspace_id UUID)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
  DELETE FROM workspaces WHERE id = workspace_id;
END;
$$;

Congratulations. Any authenticated user can call rpc('delete_workspace', {workspace_id: 'target-uuid'}) and nuke ANY workspace in your database. RLS policies are completely ignored because the function runs as superuser.

Here's the harsh truth. If you must use SECURITY DEFINER, you have to manually enforce the checks inside the function, or set the search_path and drop privileges. But honestly? Just use SECURITY INVOKER. It respects your RLS policies out of the box. Stop bypassing your own security.

The Infinite Recursion Death Loop

This isn't just a security issue; it's a denial-of-service vulnerability you built yourself.

You have a teams table and a team_members table. You want users to see teams they are a member of.

So you write a policy on teams:

sqlSource Code
CREATE POLICY "View teams"
ON teams FOR SELECT
USING (
  EXISTS (
    SELECT 1 FROM team_members 
    WHERE team_members.team_id = teams.id 
    AND team_members.user_id = auth.uid()
  )
);

Honestly, it's a nightmare. Then you write a policy on team_members that references teams to check if the user is an admin.

Boom. Infinite recursion. Postgres will try to evaluate the policy, which queries the other table, which evaluates its policy, which queries the first table. Your database CPU spikes to 100%. The query times out. The app goes down.

Attackers can trigger this intentionally. It's a trivial DoS.

The fix? Use a SECURITY DEFINER function strictly for looking up permissions, bypassing RLS just for the lookup, and caching the result. Or better yet, redesign your schema so policies don't cross-reference each other.

The Leak via Views

Honestly, it's a nightmare. Did you know PostgreSQL views don't enforce RLS by default if they were created before PG 15 without the security_invoker flag?

You create a view for a dashboard.

sqlSource Code
CREATE VIEW public_dashboard AS
SELECT id, name, revenue FROM sensitive_sales_data;

You secure sensitive_sales_data with RLS. But the view executes as the view owner. An attacker queries the view via the Supabase API and gets all the data.

You need to explicitly create the view with security invoker:

sqlSource Code
CREATE VIEW public_dashboard 
WITH (security_invoker = true)
AS SELECT id, name, revenue FROM sensitive_sales_data;

How Bryxe Nixes This Nonsense

Honestly, it's a nightmare. Look, I get it. Row Level Security best practices are hard to keep track of. You have 50 tables, 200 policies, and a junior dev who just learned SQL yesterday.

Manual audits don't scale. You need a Supabase security audit tool that actually understands the AST of your Postgres policies.

That's what we built at Bryxe. Our DevSecOps platform hooks directly into your CI/CD pipeline and your Supabase staging environment.

We parse every CREATE POLICY statement. We flag TO authenticated missing auth.uid() checks. We detect SECURITY DEFINER functions lacking explicit RLS enforcement. We simulate Postgres RLS bypass vectors before they hit production.

I'm tired of seeing this in PRs. Bottom line: If you're running Supabase in prod without automated policy testing, you're flying blind.

ROI of Automated Auditing

Let's talk numbers. A data breach costs an average of $4.45M. A manual pentest costs $15k and is outdated the second you merge the next PR.

ApproachCostRisk WindowCoverage
Manual Code ReviewHigh (Engineering hours)WeeksSpotty
External Pentest$15k - $30kMonthsPoint-in-time
Bryxe DevSecOpsFraction of a pentestMinutes100% of commits

Stop paying humans to find missing auth.uid() checks. Automate that garbage.

The Wrap Up

Postgres RLS is powerful. It's exactly how multi-tenant apps should be built. But it's unforgiving. A single missing WHERE clause isn't just a bug; it's a catastrophic vulnerability.

Honestly, it's a nightmare. Audit your policies. Fix your RPCs. Nuke your unauthenticated views. And for the love of God, stop assuming TO authenticated means a user can only see their own data.

Stay paranoid.

AUTOMATED DEFENSE

Don't wait for an exploit to audit your codebase

Review supported code risks, exposed secrets and dependency findings with Bryxe Shield. Verify the fixes in your application before release.

Need a practical next step? Explore the security field guides or read our editorial and sourcing policy.

Recommended Security Research