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.
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.