vibe-supabase-rls-permissive
| Category | Default severity | Lifecycle | Default confidence |
|---|---|---|---|
| security | BLOCKER | experimental | 0.9 (clamped to 0.6 while experimental) |
What it catches
CREATE POLICY statements whose USING or WITH CHECK clause is literally true. The matcher is case-insensitive, allows arbitrary whitespace, and tolerates one layer of extra parentheses, so all of these fire:
USING (true)
USING ( TRUE )
USING((true))
WITH CHECK (true)
WITH CHECK ( true )SQL comments are stripped before matching, so a comment that mentions USING (true) will not trigger.
Why it matters
USING (true) lets every row through the read filter. WITH CHECK (true) lets every row through the write filter. Either is functionally identical to having no RLS at all, but harder to catch in review because the migration looks protected — ALTER TABLE ... ENABLE ROW LEVEL SECURITY is right there, then a policy is right there, then the policy quietly disables itself.
This is the trap right behind the no-RLS one. An audit of 50 vibe-coded apps found 24% had policies of this shape. It is also the pattern most often generated by AI coding tools when prompted to "add an RLS policy" without further guidance — the simplest legal policy that compiles is USING (true).
Example: failing code
create policy "anyone reads profiles"
on profiles for select
using (true);
create policy "wide open"
on profiles for all
using (true)
with check (true);The rule reports one BLOCKER per permissive clause. The second policy above produces two findings (one for using (true), one for with check (true)).
Example: how to fix
create policy "users read own profile"
on profiles for select
using (user_email = auth.jwt() ->> 'email');
create policy "users update own profile"
on profiles for update
using (user_email = auth.jwt() ->> 'email')
with check (user_email = auth.jwt() ->> 'email');Common Supabase patterns:
| Goal | Expression |
|---|---|
| Match the authenticated user by id | user_id = auth.uid() |
| Match the authenticated user by email | user_email = auth.jwt() ->> 'email' |
| Multi-tenant by membership table | tenant_id IN (SELECT tenant_id FROM members WHERE user_id = auth.uid()) |
| Role-based admin override | auth.jwt() ->> 'role' = 'admin' |
When USING (true) is intentional
Public-read tables (a published-articles list, a public leaderboard, etc.) genuinely want every row visible. In that case, document the intent in a comment and suppress the rule on that line:
-- This table is public-read by design (RFC-014).
-- codemore-ignore-next-line: vibe-supabase-rls-permissive
create policy "anyone can read published posts"
on published_posts for select
using (true);That keeps the policy in the migration but takes responsibility for the choice in writing.
Known limitations
- The matcher only recognises the literal
true. Tautologies like1 = 1,NOT false, or'a' = 'a'are not yet caught. A v1.1 expression-aware pass is planned. - We do not check whether the table being targeted has any other policy that further restricts access. A second, scoped policy on the same table does not make the permissive one safer — RLS policies combine with OR, so the permissive one wins.
- Cross-file scope: we do not detect a permissive policy created in a different migration file than the table.
Suppression
-- codemore-ignore: vibe-supabase-rls-permissive
create policy "wide open" on profiles for select using (true);File-wide:
/* codemore-ignore-file: vibe-supabase-rls-permissive */References
- Supabase — Writing RLS policies
- PostgreSQL — CREATE POLICY
- DEV — I Audited 50 Vibe-Coded Apps. Here's What Broke.