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