MP057critical

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

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

migrationpilot analyze migration.sql
✗ [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:

.migrationpilotrc.yml
rules:
  MP057: false

# or keep it, and downgrade it
rules:
  MP057:
    severity: warning

Try 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

Related rules