Supabase RLS bug classes — reference
This page catalogs the RLS bug classes that don’t get full case studies in the Inbox sample. One paragraph per bug, one short SqlProof property snippet, plus an audit-style snippet for the schema-level ones.
For the fuller, recipe-style treatment of the most-impactful RLS bugs (tenant scoping in SECURITY DEFINER RPCs, uncorrelated EXISTS, missing WITH CHECK, missing DELETE policies, internal-vs-public message gating), see the Inbox sample recipes.
Over-permissive policy (USING (true))
Section titled “Over-permissive policy (USING (true))”A policy USING (true) on a SELECT/INSERT/UPDATE/DELETE grants the operation to any caller in the policy’s role. Sometimes intentional for public-read tables, often a copy-paste error from a “make it work first” prototype.
UPDATE policy without a paired SELECT policy
Section titled “UPDATE policy without a paired SELECT policy”PostgreSQL evaluates UPDATE policies by first reading the row (via the SELECT policy chain). With no SELECT policy on the table, the read returns nothing, and the UPDATE silently affects zero rows even for the owner.
security_invoker view bypass
Section titled “security_invoker view bypass”Pre-PG15, views run as their creator (often postgres), so the underlying table’s RLS is ignored. In PG15+ create views with WITH (security_invoker = true).
user_metadata trust
Section titled “user_metadata trust”Supabase exposes auth.jwt() -> 'user_metadata' as JWT claims that the client can write. Policies that trust user_metadata->>'role' = 'admin' are bypassable; use app_metadata (server-only) or a database table.
SqlProof’s as_supabase_user accepts an extra_claims= arg, so you can directly test that a user with an admin claim in user_metadata doesn’t gain admin powers:
Infinite policy recursion
Section titled “Infinite policy recursion”A policy on table A that references table B, whose policy references table A, can cause PostgreSQL to recurse until stack depth limit exceeded. The property: every policy-gated query completes within a reasonable budget.
The Inbox sample’s Recipe 2 includes a comment about this pattern — the recursive form of org_members self-reference is exactly this class.
Schema-level audits (one-shot, not property tests)
Section titled “Schema-level audits (one-shot, not property tests)”These are introspection queries, not invariants — run them once per CI build to enforce shop-wide RLS hygiene.
Every public table has RLS enabled
Section titled “Every public table has RLS enabled”Every policy specifies a TO clause
Section titled “Every policy specifies a TO clause”Every SECURITY DEFINER function pins search_path
Section titled “Every SECURITY DEFINER function pins search_path”When SqlProof ships issue #77 (planned sqlproof.contrib.supabase.audit module), these snippets will become one-liners like assert_rls_enabled(db, "tickets") and tables_without_rls(db) == set().
Identity / silent-fail bugs
Section titled “Identity / silent-fail bugs”When auth.uid() returns NULL (anonymous request) and a policy like USING (auth.uid() = user_id) evaluates to NULL → false, the API returns an empty result. Test the anonymous path explicitly — see SqlProof issue #78 for the planned as_anonymous() helper; today’s workaround: