Skip to content

Fix critical privilege escalation via function/operator overloading and search_path hijacking - #29

Open
mdisec wants to merge 2 commits into
RhodiumToad:masterfrom
mdisec:security/qualify-extension-script-references
Open

Fix critical privilege escalation via function/operator overloading and search_path hijacking#29
mdisec wants to merge 2 commits into
RhodiumToad:masterfrom
mdisec:security/qualify-extension-script-references

Conversation

@mdisec

@mdisec mdisec commented Aug 3, 2026

Copy link
Copy Markdown

Hey,

I have found this vulnerability during a security research for a managed-Postgres provider and I actually successfully exploited this to escalate priv to rolsuper.

The bug is like if an untrusted user can create objects in a schema, they can plant exact-typed function or operator overloads, such as unnest(text[]) or =(oid, regclass). Later PostgreSQL may select these instead of the pg_catalog implementations, allowing attacker-controlled SQL to execute as the superuser installing or upgrading ip4r extension.

I used claude to prepare a fix for this bug, please feel free to make changes on the PR and do necessary changed you want.

mdisec and others added 2 commits August 3, 2026 10:18
PostgreSQL replaces the caller's search_path while running an extension
script, setting it to the extension's target schema.  For a relocatable
extension installed the usual way that schema is public, so any object
a low-privileged user can create there is visible to the script, which
runs as the superuser performing CREATE EXTENSION or ALTER EXTENSION.

pg_catalog is searched implicitly ahead of the rest of the path, so a
planted function with a signature identical to a catalog one loses.  An
exact-typed overload competing against a polymorphic catalog function
does not: function resolution prefers the exact match before it ever
considers schema order.  A user with CREATE on public can therefore
define public.unnest(text[]) and have it called in place of
pg_catalog.unnest(anyarray), executing an arbitrary body as superuser
while the install completes without error.

ip4r--2.4.sql was exploitable on any install against PostgreSQL 11 or
later via the UNNEST(ARRAY[...]) in the operator family fixups.  The
2.2--2.4 update script was exploitable through UNNEST over its oid[]
and text[] locals, and through the polymorphic array operators || and
<>.  The 2.0--2.1 and 2.1--2.2 scripts were exploitable through
array_to_string over an oid[].

Qualify the function calls, replace the two vulnerable operators with
OPERATOR(pg_catalog.<>) and pg_catalog.array_append(), and qualify the
catalog table references while here.  This completes the sweep started
in b07bf1d, which qualified the format() calls only.

Left unqualified references such as current_setting() and
pg_get_indexdef() are not hijackable, since their catalog signatures
are exact and pg_catalog wins ties, but they are qualified too so the
scripts can be audited by inspection rather than by case analysis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous commit qualified function calls and the two polymorphic
array operators, but missed a second instance of the same hazard.

Where a catalog column of type oid is compared against a ::regclass or
::regtype constant, pg_catalog has no operator for that argument pair,
so a planted public.=(oid,regclass) is the only exact match and wins
outright.  Unlike the unknown-literal comparisons elsewhere in these
scripts -- name = 'x', "char" <> 'i' and so on, where the catalog's own
operator takes the tie -- there is nothing for it to tie against.

The reachable case is the dependency scan in ip4r--2.2--2.4.sql.  With a
dependent hash index present, which is the situation the whole update
script exists to handle, a planted =(oid,regclass) is evaluated and its
body runs as the superuser performing ALTER EXTENSION.  Confirmed by
escalating a low-privileged role to superuser on 18.4.

Use OPERATOR(pg_catalog.=) and OPERATOR(pg_catalog.<>) there.

The IN (...::regprocedure) lists are not capturable -- an IN list of two
or more elements becomes = ANY over an array and resolves to oid = oid
-- but that depends on subtle resolution behaviour, so compare oid to
oid explicitly and remove the question.  The row comparison against
::regtype in ip4r--2.0--2.1.sql is likewise not an escalation route,
because a planted operator outside a btree family makes the row
comparison fail to resolve, but that is an unauthenticated denial of
service on the update, so cast there as well.

Also restore the continuation-line alignment disturbed by the previous
commit, where qualifying a call shifted the opening element right but
left the following lines behind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant