Re: Prevent object capture in CREATE/ALTER EXTENSION scripts

From: Osama Abdul Qader <osamaabdulqader(dot)cs(at)gmail(dot)com>
To: Jan Nidzwetzki <jan(at)planetscale(dot)com>
Cc: PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Subject: Re: Prevent object capture in CREATE/ALTER EXTENSION scripts
Date: 2026-09-07 15:16:50
Message-ID: CAC+8b5iXC6MSmABg7wtoWQeUqGKGV+1SfC43F3o5Jnw+mJS=rg@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi Jan,

I'm interested in looking into this issue, I'll sure to reproduce it in my
machine tomorrow and let you know about it.

Best Regards,
Osama Abdul Qader

On Mon, 7 Sept, 2026, 8:09 pm Jan Nidzwetzki, <jan(at)planetscale(dot)com> wrote:

> Hi Hackers,
>
> We would like to propose hardening name resolution during extension
> installation and upgrade scripts, so that a script cannot be made to
> reach an object that another role planted in the extension's
> installation schema.
>
>
> Problem
> =======
>
> A CREATE EXTENSION or ALTER EXTENSION ... UPDATE script runs as the
> invoking role, or as the bootstrap superuser for a trusted extension
> installed by a non-superuser. Either way a captured reference runs
> another role's code with elevated rights.
>
> Say the script calls f('abc'). The literal has type "unknown", so if
> the extension defines f(varchar) but another role has already created
> f(text) in the same schema, both are candidates. They sit in the same
> schema, so search-path position cannot break the tie, and
> func_select_candidate() falls back to type preference: text is the
> preferred type of the string category, so f(text) wins, and the script
> runs that role's function with its own privileges. Writing
> @extschema(at)(dot)f('abc') changes nothing: qualification pins the schema the
> plant sits in, not the signature.
>
> Neither existing defense covers this. The search_path pinning from
> 7eeb1d9861b ("Make contrib modules' installation scripts more
> secure.") only keeps the plant out of the other schemas on the path,
> not out of the extension's own target schema, which is necessarily
> first. And the ownership checks from b9b21acc766 ("In extensions,
> don't replace objects not belonging to the extension.") guard what a
> script creates, not what it references.
>
> The same applies to operators: CREATE OPERATOR only requires a function
> the creating role can execute. Nor is it limited to overloads: a domain
> or table planted under the name of a required extension's type or config
> table captures the reference outright, and its CHECK constraint or
> triggers then run with the script's privileges.
>
> The only precondition is that some other role can create objects in
> the installation schema. Hosted platforms that run whitelisted
> extension scripts as superuser on a user's behalf make that the normal
> case, not an odd configuration.
>
> This hazard is documented: "Security Considerations for Extension
> Scripts" describes these trojan objects and tells authors to
> schema-qualify every name and add explicit casts [1]. That advice is
> correct, but it puts the whole burden on the author getting every call
> site right, and one uncast call is enough, so a resolution-time
> backstop seems worthwhile.
>
>
> Proposed change
> ===============
>
> Two patches attached:
>
> 0001 - propagates the extension-script state to parallel workers.
> creating_extension and CurrentExtensionObject are backend-local
> globals, so a worker never saw them. Work that a worker does on behalf
> of a script, such as parse-analyzing a parallel-safe function's body at
> run time, therefore ran as though no script were in progress and would
> skip the check that 0002 adds. Both values now ride through
> FixedParallelState the same way the current user id and the
> temp-namespace state already do. No visible effect on its own.
>
>
> 0002 - makes name resolution ignore untrusted objects while
> creating_extension is set. An object is trusted if it is in pg_catalog,
> owned by a superuser, owned by the role the script is running as, or a
> member of the extension being installed or of one it requires; the last
> rule reads pg_depend, and only where the cheaper tests fail.
>
> The search-path lookups for relations, types, functions and operators
> all apply the test. For functions and operators it happens as
> candidates are gathered, not in the callers that resolve the
> ambiguity: the gather loop collapses duplicate signatures to the one
> earliest on the path, so a plant in the extension's own schema would
> otherwise displace a required extension's identical function. It is
> also the single place every caller passes through, including
> LookupFuncNameInternal() and regprocedure input; the operator test in
> OpernameGetOprid() likewise covers binary_oper_exact(),
> LookupOperName(), and so CREATE OPERATOR CLASS, and regoperator.
>
> Caches need the same treatment. oper() and left_oper() skip the
> operator lookaside cache while creating_extension, and CachedPlanSource
> records whether it was analyzed inside a script, including on the
> plpgsql simple-expression fast path, since a plan analyzed beforehand
> in a session whose search_path matches the one the script pins would
> otherwise be reused inside it.
>
> If nothing trusted remains, resolution fails as though the object did
> not exist, with a detail saying a candidate was ignored. If a trusted
> candidate exists but does not match, the usual argument-type error is
> raised and the ignored candidate is mentioned in a hint. Outside
> extension scripts every object is trusted, so ordinary parsing is
> unchanged.
>
> 0002 adds regression tests for planted overloads, references with no
> trusted candidate, "superuser = false" extensions, updates run by
> another role, required extensions, cached plans and parallel workers.
>
>
> What this does not cover
> ========================
>
> The filtering only applies while the script runs. An extension's own
> function bodies resolve names when they execute, with
> creating_extension false again, so a planted overload can still
> capture those calls after installation, usually with the caller's
> privileges, though SECURITY DEFINER puts the elevated case back on the
> table. Covering that wants a different mechanism; script time seems
> worth doing on its own, being the window where a captured reference is
> most likely to run as a superuser.
>
> Trust is by ownership, not by name. A script that references an object
> owned by another ordinary role, including one that belongs to an
> extension outside its direct requires list, now fails even with a
> schema-qualified name. Such references should be rare, but this is a
> behavior change for existing scripts.
>
> Operator classes and families, collations, text search objects,
> conversions, statistics objects and casts still take unfiltered lookup
> paths; none looked like a route to running an unprivileged role's code,
> but we may have missed one. The lookups that resolve by exact name also
> have no flags word to carry the "candidate ignored" detail.
>
>
> Related work
> ============
>
> Jelte Fennema-Nio's "extensions with an owned schema" [2] starts from
> the same observation and gives the extension a fresh schema, removing
> the precondition. It is opt-in and only for new extensions, so the two
> look complementary.
>
> The "sandboxing untrusted code" thread [3] makes a point we tried to
> honor: a check that rejects what a human reads as harmless gets
> switched off. A script references built-ins, its own objects and its
> required extensions', and the trust rule accepts all three regardless
> of who owns them.
>
>
> Does this approach seem reasonable?
>
>
> [1]
>
> https://www.postgresql.org/docs/current/extend-extensions.html#EXTEND-EXTENSIONS-SECURITY-SCRIPTS
> [2]
>
> https://www.postgresql.org/message-id/flat/CAGECzQQzDqDzakBkR71ZkQ1N1ffTjAaruRSqppQAKu3WF%2B6rNQ%40mail.gmail.com
> [3]
>
> https://www.postgresql.org/message-id/flat/CA%2BTgmoYiumw-yR8nUUX_8qdihPd0ZmT29ch0VR_r%2Bkw%2Bo7QJvQ%40mail.gmail.com
>
>
> Best regards
> Jan Nidzwetzki
> Fabrízio de Royes Mello
>
> --
> Jan Nidzwetzki
> PlanetScale Postgres Core Team
>

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Alexander Korotkov 2026-09-07 15:22:22 Re: Two issues leading to discrepancies in FSM data on the standby server
Previous Message Palak Chaturvedi 2026-09-07 15:01:20 Re: Make pg_prewarm, autoprewarm yield for waiting DDL