A wrong RLS policy doesn't throw an error, it just returns the wrong rows, so it ships untested and you find out when data leaks. rlsautotest gener...
For further actions, you may consider blocking this person and/or reporting abuse
The zero-rows versus permission-error split is a distinction most write-ups skip, glad you kept them apart. Does the generator also walk views and security definer paths? The leaks I've hit lived there, not in the table policies.
Straight answer: not yet. It walks base tables and their policies today, not views or SECURITY DEFINER paths. Both are exactly where rights get swapped for the owner's: a view without security_invoker runs as its owner and skips the base table's RLS, and a SECURITY DEFINER function runs as the definer unless it re-checks auth. A tool that stops at the table catches neither, which is why that's where I want to take it next. If you've got a sanitized example that bit you, I'd love it as a test case.
Here's a clean one to seed: a base table with per-user RLS, then a reporting view over it for some dashboard, created without security_invoker. The view runs as its owner, so PostgREST serves it straight to anon and every user's email comes back, and the base table's RLS never gets consulted. The tell you could assert on is that exact pair: a view whose base table has relrowsecurity and policies, but the view itself isn't security_invoker. Green base table, wide-open view.
Ok. I will work around your description to create the scenario. This should be mostly be fixed by next week. Will keep you posted. MEanwhile if you want to track it in github, i would encourage you to log enhancement over there. Here is the repo link- github.com/unitautogen/rlsautotest
@vollos - One ask if you are ok to answer. Do you face issues around unit testing your functions and triggers? How do you ensure proper code coverage and track the metrics?
Glad the view case is worth chasing. On your question, honest answer: coverage metrics aren't really my beat. I spend more time reviewing other people's code for security than maintaining a big suite of my own, so I'm not the one with a coverage regime to model. From the review side though, functions and triggers are the least-tested surface I see in AI-built Supabase apps, usually zero tests at all. And where there is coverage, line coverage misses the case that bites: whether the function ever ran as an identity that shouldn't be allowed. A SECURITY DEFINER function can be 100% line-covered and never once exercised against the wrong caller. So the thing I'd track is coverage by identity, not by lines.
Silent failures in RLS are definitely one of the biggest foot guns when migrating from a traditional backend to Supabase. I usually tackle this by writing custom PL/pgSQL functions that explicitly assert the expected row counts and throw exceptions if the policy logic falls short during CI. Another approach that has saved me a lot of debugging time is using the Supabase CLI to run local migrations with seed data specifically designed to trigger edge cases in the policies. Have you found a specific testing library or framework that integrates better with Jest or Vitest for mocking the auth context during these RLS tests?
Honestly, I haven't found a Jest/Vitest library I'd trust here, because there's nothing to mock. auth.uid()/auth.jwt() just read request.jwt.claims and the current role, so the only faithful way to impersonate is on a real session: SET LOCAL ROLE authenticated + set_config('request.jwt.claims', '{"sub":...}', true), query, assert, roll back. Mock it in JS and you're testing the mock, not the policy. From Vitest that's a pg client, one transaction per test, set role and claims, assert, rollback. It's also why I emit pgTAP: the assertion runs in the same session as the policy, so nothing gets faked.
Your instincts are already right, by the way. The throwing PL/pgSQL asserts are hand-rolled pgTAP, and those edge-case seeds are the part that quietly rots when a policy changes.
The seed-data point is the one most hand-rolled suites get wrong, and it deserves the billing you gave it — an assertion that passes against an empty table is exactly how these end up green and worthless.
Two things worth adding for readers.
"The SQL editor bypasses RLS" is not quite the reason it misleads you.
The editor connects as
postgres, and RLS is skipped for a role holdingBYPASSRLSor for the table owner (absentFORCE ROW LEVEL SECURITY). Butset role authenticateddrops both of those, and from that point policies are enforced — which is why theset rolepattern works at all.What actually makes editor testing lie is narrower, and fixable:
request.jwt.claimsis unset, soauth.uid()andauth.jwt()evaluate against NULL rather than against a user. A policy ofusing (user_id = auth.uid())then filters everything away, and you conclude it is too strict when it is fine.Scoped to a transaction so it cannot leak into the rest of the session:
Swap the
suband you have the cross-tenant check by hand. That does not replace a generated suite, but it is worth knowing the editor is usable rather than useless — "you cannot test this here" is the reason a lot of people never test it anywhere.What a per-identity matrix structurally cannot show
The report answers "can identity X touch table Y", which is the right question for policies. The leak it cannot see is the one that goes around them: a
SECURITY DEFINERfunction executes as its owner, and a view in Postgres runs with the view owner's rights unless you setsecurity_invoker = true— so the caller's policies never run. One helper that returns rows from a tenant-scoped table hands every caller the whole table, and every cell in the matrix still reads correctly, because the policy is correct. It simply is not in the path.Worth a companion check against the catalog rather than against the policies:
Anything security-definer owned by
postgresorsupabase_adminand callable byanon, and any view withoutsecurity_invoker=trueover an RLS table, is worth reading line by line.Correcting myself: the second half of my comment above is wrong.
I said a per-identity matrix structurally cannot show the bypasses that go around policies. rlsautotest reports exactly those, and has since v0.3.0 — there is a "Beyond the policies: bypass surfaces" section covering owner-rights views and materialized views readable by
anonorauthenticatedover an RLS-protected table,SECURITY DEFINERfunctions a client canEXECUTE, mutablesearch_pathon those functions,BYPASSRLSand superuser roles, and RLS-enabled-but-not-FORCEd tables. It judges reachability by effective privilege viahas_table_privilege/has_function_privilege, which picks upPUBLICgrants — and a function reachable only through Postgres's defaultPUBLICEXECUTEgrant is precisely what a hand-written check tends to miss, because nobody granted it explicitly.I read the article and not the README before writing that. The catalog queries are still fine as a manual check; the claim that the tool needs them is not.
The
set rolecorrection in the first half stands.