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