SEC.RLS.POLICY_ALWAYS_TRUE — A policy that lets every row through
- Category: security
- Severity: high
- Level: 0
- Confidence: deterministic
- Downtime class: none — the finding is about a policy, not about a statement
- Stability: stable
- Suites: audit
- Applies to: PostgreSQL 18
The state that passes every review
The table has row-level security enabled. It has a policy. \d+ shows both. And every row is visible
to everybody, because the policy's filter is a constant that is always true.
USING (true) is what somebody writes to get a feature working, intending to come back to it. Nothing
afterwards looks wrong.
One always-true policy is enough
Permissive policies are OR-ed. A table with a careful tenant policy and one USING (true) policy
is exactly as exposed as a table with only the second — the careful one contributes nothing to the
outcome. So the finding names the offending policy: that is the one to open, and fixing the other one
changes nothing.
A restrictive policy cannot cause this. It is AND-ed and can only narrow, so SQLens does not count
one. It can cure it, though: a row is visible when some permissive policy passes and every
restrictive one does, so a permissive USING (true) base with a restrictive tenant policy is the
documented way to write tenant separation, and not a finding. Measured on PostgreSQL 18.4, a tenant
reads 1 of 3 rows through that pair and cannot write another tenant's row. The restrictive policy has
to apply to every role and command the base does: one only FOR SELECT, or only for another role,
leaves the rest open, and the finding stays.
What SQLens will not decide
It recognizes the constant forms PostgreSQL deparses an always-true filter into — measured against the server, not guessed:
| You write | The catalog stores |
|---|---|
USING ( TRUE ) | true |
USING ((true)) | true |
USING ('t'::bool) | true |
USING (1 = 1) | (1 = 1) |
Anything with a function call, a current_setting() comparison or a subquery is a filter SQLens does
not evaluate — including expressions that happen to be always true, such as (1 = 2) OR (3 > 2).
It says nothing about those rather than guessing. A rule that tried to decide whether an arbitrary
expression can ever be false would be wrong occasionally and confident always, which in a security
suite is worse than being silent.
Bad
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders USING (true); -- placeholder that shipped
Good
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant')::uuid);
Related
SEC.RLS.DISABLED covers the table with no row-level security at all;
SEC.RLS.CHECK_ALWAYS_TRUE covers the narrower case where
only the write path is unguarded.