Skip to main content

SEC.RLS.NO_POLICY — Row-level security is on and there is no policy

  • Category: security
  • Severity: medium
  • Level: 0
  • Confidence: deterministic
  • Downtime class: none — the finding is about a table's state, not about a statement
  • Stability: stable
  • Suites: audit
  • Applies to: PostgreSQL 18

The accident that only shows up in production

Somebody enabled row-level security, meant to write the policy next, and the deploy went out. With RLS on and no policy, PostgreSQL denies every row — this is the one RLS state where nothing is exposed and the data is simply unavailable.

Why it survives development is the role it is seen from. Measured against PostgreSQL 18.4, a table with two rows, RLS enabled and no policy:

Connecting asRows returned
the table's owner, without FORCE2 — the owner's exemption applies
the table's owner, with FORCE0
any other role0

So whoever created the table sees it working. The migration passes, the seeder passes, the test suite passes if it runs as the owner — and the application, connecting as a different role, gets empty result sets with no error anywhere.

Why medium

Nothing can leak: no tenant reads another's rows, because nobody reads any. This is an availability finding, not a disclosure one, and the severity says so rather than borrowing weight from the rules around it.

Bad

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- …and no CREATE POLICY, ever

Good

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant')::uuid);

Or, if the table was switched on by mistake:

ALTER TABLE orders DISABLE ROW LEVEL SECURITY;

The FORCE rules (SEC.RLS.NOT_FORCED, SEC.RLS.OWNER_UNRESTRICTED) stay silent on a table with no policy: "your policies are being bypassed" is not a true sentence about a table that has none.