RLS bypass: ON CONFLICT DO UPDATE/SELECT evaluates WHERE before RLS check

From: Joao Detomini <joao(dot)detomini(at)enterprisedb(dot)com>
To: pgsql-hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Subject: RLS bypass: ON CONFLICT DO UPDATE/SELECT evaluates WHERE before RLS check
Date: 2026-10-06 18:53:14
Message-ID: CABH8dKzYzRKD4izbushndHvvsEQg9b6m3w9ncYAGMbZ5m+9gNg@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

While testing ON CONFLICT DO SELECT on master, I found that the
user's WHERE clause is evaluated against the existing row before the
row is checked against the RLS policies. A user who cannot see a row
through SELECT/UPDATE policies can therefore read its columns, as long
as a proposed row conflicts with it on a unique key.

Both ExecOnConflictUpdate() and ExecOnConflictSelect() in
nodeModifyTable.c do ExecQual(onConflict*Where) first and only then
ExecWithCheckOptions(WCO_RLS_CONFLICT_CHECK). The CREATE POLICY docs
say the existing row is checked against the USING expressions first.

Repro (attached repro.sql, as superuser; then as a role that can't see
row 2):

insert into oc values (2,'u_sel','x')
on conflict (id) do select where leak(oc.secret) returning id;
NOTICE: LEAKED: HIDDEN-VALUE

where leak() is a plpgsql function that does RAISE NOTICE. The same
works with DO UPDATE. Without any function, a plain predicate is an
oracle: "where oc.secret like 'HID%'" raises the RLS error when it
matches and is a silent no-op when it doesn't. The attacker needs to
hit the hidden row's key, which is easy to enumerate for serial ids.

DO UPDATE has had this ordering since 168d5805e4 (2015); DO SELECT
(88327092ff) copied it. I have only tested master and 16beta2, I haven't
tested the other stable branches.

The attached patch swaps the order in both functions, so RLS is checked
before the WHERE clause. With it the repro no longer leaks, and the
regress suite passes. No existing test covers this, so it also needs
a regression test. One behavior change: a row failing the RLS check
now raises the error even when WHERE would have been false. That
matches what the docs describe.

Thanks,
João Marcelo

Attachment Content-Type Size
0001-Check-RLS-policies-before-evaluating-the-ON-CONFLICT.patch application/octet-stream 8.4 KB
repro.sql application/octet-stream 1.4 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message Manu 2026-10-06 19:00:29 Re: Proposal: "query_work_mem" GUC, to distribute working memory to the query's individual operators
Previous Message Sehrope Sarkuni 2026-10-06 18:44:00 Bug: ATTACH PARTITION can leave rows violating default partition constraint