6 min read

Supabase RLS misconfiguration — detect, exploit, and fix

Row-Level-Security bypass is the most common data leak in vibe-coded apps. Here is exactly how it happens, how attackers find it, and how to fix it in Next.js + Supabase with one policy update.

If you are shipping a Supabase-backed app and you have ever typed `select * from orders where user_id = auth.uid()`, you probably have a Row-Level-Security bug waiting to be found. This guide walks through the exact pattern, a proof-of-concept exploit, and the one-line fix.

What it is

Supabase Row-Level-Security (RLS) is a Postgres feature that restricts which rows a user can read or write. When RLS is not enabled on a table, or the policy does not scope by tenant, any authenticated user can read any row — including rows that belong to other customers of your app.

Vulnerable example

-- orders table with single-tenant RLS policy (WRONG for multi-tenant apps)
create policy "users can read their own orders"
on orders for select
using (auth.uid() = user_id);

-- In a multi-tenant app this still lets tenant-A read orders owned by
-- tenant-B, as long as they share a user_id. It does not scope by tenant.

Fixed example

-- Scope by tenant as well as user
create policy "users read orders in their tenant"
on orders for select
using (
  auth.uid() = user_id
  and tenant_id = (auth.jwt() ->> 'tenant')::uuid
);

How Securie catches it

Securie's Supabase RLS specialist agent reads your migrations, derives the intent of each policy against your app's intent graph, and spins up a sandbox copy of your database with multi-tenant fixtures. It then issues cross-tenant reads through your actual API routes. If data leaks, Securie writes the corrected policy as a pull-request comment you can merge in one tap.

Checklist

  • Every multi-tenant table has RLS enabled (`alter table <t> enable row level security`)
  • Every policy scopes by both `auth.uid()` and a tenant identifier
  • The tenant identifier comes from the JWT, not from a user-supplied parameter
  • Policies exist for all four verbs — select, insert, update, delete
  • Service-role keys are never used from the client
  • New tables have RLS enforced by default via a migration linter

FAQ

Is RLS enough on its own?

RLS is a strong primary defense but should be paired with API-layer authorization checks. If an attacker ever reaches your database with a compromised service role key, RLS will not save you.

How do I test RLS locally?

Use `supabase db test` with fixtures for multiple tenants, then run your API routes against each fixture and assert you only see rows owned by the current tenant. Or connect your repo to Securie and we run this on every pull request.