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 as | Rows returned |
|---|---|
the table's owner, without FORCE | 2 — the owner's exemption applies |
the table's owner, with FORCE | 0 |
| any other role | 0 |
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;
Related
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.