Skip to main content

SEC.RLS.CHECK_ALWAYS_TRUE — Reads are separated, writes are not

  • Category: security
  • Severity: medium
  • 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 half that gets forgotten

A policy's USING expression decides which rows are visible. Its WITH CHECK expression decides which rows may be written. When the filter is correct and the check is true, a tenant sees only its own rows and can still insert or update a row carrying somebody else's id.

Nothing in the application notices: the write succeeds. The row lands under another tenant's id and becomes invisible to the one that created it. It surfaces much later as data nobody can account for.

An omitted WITH CHECK is not this finding

PostgreSQL applies the USING expression to writes when no WITH CHECK is given. A policy with a real filter and no explicit check is therefore guarded on both paths — and that is how almost every correct policy is written.

Only an explicit always-true check is a hole, because somebody had to type it.

Bad

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

Good

-- Either state the same condition for writes…
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant')::uuid);

-- …or leave WITH CHECK off entirely and let PostgreSQL reuse USING.
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant')::uuid);

SEC.RLS.POLICY_ALWAYS_TRUE covers the larger case, where the read path is open as well. A policy that admits everything on both paths is reported only there — saying it twice would bury this finding under a repetition of the bigger one.