Skip to content

vibe-supabase-rls-permissive

CategoryDefault severityLifecycleDefault confidence
securityBLOCKERexperimental0.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:

GoalExpression
Match the authenticated user by iduser_id = auth.uid()
Match the authenticated user by emailuser_email = auth.jwt() ->> 'email'
Multi-tenant by membership tabletenant_id IN (SELECT tenant_id FROM members WHERE user_id = auth.uid())
Role-based admin overrideauth.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 like 1 = 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

Next →
Back to the catalog
See the other 57 rules — grouped by pack, with lifecycle gates.