rls-enabled-without-policy
ENABLE ROW LEVEL SECURITY without a matching CREATE POLICY silently blocks all access.
- operation
- Privileges and RLS
- lock taken
- no table lock
- remediation
- Manual rewrite
- category
- Privileges & RLS
- Breaks a rolling deploy
What triggers it
An AlterTableStmt with a command of subtype AT_EnableRowSecurity, where no statement in ctx.allStatements passes a text test. That test lowercases each statement and requires it to contain both create policy and the target table name as substrings — it does not parse CreatePolicyStmt or compare the policy target table.
What does not
FORCE ROW LEVEL SECURITY, which parses to the separate subtype AT_ForceRowSecurity and is never checked — verified against the bundled parser. DISABLE ROW LEVEL SECURITY. Any file where some statement contains both magic substrings, including a policy written in a comment, since the scan is over raw text.
Where it applies
Applies to every PostgreSQL version MigrationPilot targets. It works on the SQL text alone — no database connection needed.
The lock, and what it blocks
Not a lock rule. ALTER TABLE ... ENABLE ROW LEVEL SECURITY takes ACCESS EXCLUSIVE for a moment to flip one catalog flag and touches no data. The hazard is what happens after it commits: with RLS on and zero policies the default is deny-all, so every query from a non-superuser, non-owner role returns zero rows with no error at all. The application sees an empty table, not a permission failure.
Why it matters
When RLS is enabled with zero policies, the default behavior is a complete deny — all queries from non-superuser roles return zero rows. No error is raised. Supabase documents this as the leading cause of data lockout incidents.
Unsafe, and safe
Flagged
ALTER TABLE users ENABLE ROW LEVEL SECURITY; -- All non-superuser queries now return 0 rows!
Safe alternative
ALTER TABLE users ENABLE ROW LEVEL SECURITY; CREATE POLICY users_select ON users FOR SELECT USING (true);
What it assumes
Policy detection is substring matching in both halves, and both halves are wrong in a useful way. Table matching is a substring test, so a CREATE POLICY on users_archive satisfies the check for table users and suppresses a real violation. Cross-file state is invisible: a policy created in an earlier migration, or already present in the database, is not visible to the scan, so enabling RLS in its own migration file always reports even when the policies exist. The rule reads no catalog — nothing touches ctx.cluster — so --database-url does not let it confirm what policies the table already has.
What the CLI prints
✗ [MP057] CRITICAL (line 1) ENABLE ROW LEVEL SECURITY on "users" without a matching CREATE POLICY. With no policies, all non-superuser queries return zero rows, silently breaking the application. Safe alternative: -- Always create at least one policy when enabling RLS: ALTER TABLE users ENABLE ROW LEVEL SECURITY; CREATE POLICY users_select ON users FOR SELECT USING (true); -- Then add restrictive policies as needed. Why: When RLS is enabled with zero policies, the default behavior is a complete deny: all queries from non-superuser roles return zero rows. No error is raised. SELECT * FROM table returns empty. This silently breaks applications in a way that looks like an empty table rather than a permission error. Supabase documents this as the leading cause of data exposure/lockout incidents. Docs: https://migrationpilot.dev/rules/mp057
Generated by running the CLI's own formatter over the flagged example above, so it is the text the tool actually produces. A real run also reports the other rules that fire on the same statement; those blocks are left out here.
Turning it off
For one statement, put a comment on the line before it:
-- migrationpilot-disable MP057 ALTER TABLE users ENABLE ROW LEVEL SECURITY;
For the whole project, in .migrationpilotrc.yml — by name or by id:
rules:
MP057: false
# or keep it, and downgrade it
rules:
MP057:
severity: warningTry it
Open this rule's flagged example in the playground. It runs in your browser — edit it and watch the finding appear and disappear.
Run MP057 in the playground