From 3ea49beb8baf06f5e1b7c0551b8c0c07adcea8f5 Mon Sep 17 00:00:00 2001 From: Jim Mlodgenski Date: Thu, 11 Jun 2026 14:05:10 +0100 Subject: [PATCH] Validate identifiers substituted into extension scripts execute_extension_script() substitutes @extowner@ and @extschema@ into the extension script, filtering each value through quote_identifier(). Certain characters (" $ ' \) cannot be quoted consistently both inside and outside of string literals, so a name containing one of them can produce a script that does not parse the way the extension author intended once the substitution is performed. Reject substitution when the owner or schema name contains any of these characters instead of producing such a script. This matches the identifier handling that PostgreSQL core performs for the same substitutions. Extend pg_tle_injection with coverage for both the owner and schema cases, including that ordinary names continue to work. --- src/tleextension.c | 21 +++++++++ test/expected/pg_tle_injection.out | 75 ++++++++++++++++++++++++++++++ test/sql/pg_tle_injection.sql | 51 ++++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/src/tleextension.c b/src/tleextension.c index 87f4dae..68c7d33 100644 --- a/src/tleextension.c +++ b/src/tleextension.c @@ -1392,6 +1392,16 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, char *c_sql = read_extension_script_file(control, filename); Datum t_sql; + /* + * We filter each substitution through quote_identifier(). When the + * arg contains one of the following characters, no one collection of + * quoting can work inside $$dollar-quoted string literals$$, + * 'single-quoted string literals', and outside of any literal. To + * avoid a security snare for extension authors, error on substitution + * for arguments containing these. + */ + const char *quoting_relevant_chars = "\"$'\\"; + /* We use various functions that want to operate on text datums */ t_sql = CStringGetTextDatum(c_sql); @@ -1421,6 +1431,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, t_sql, CStringGetTextDatum("@extowner@"), CStringGetTextDatum(qUserName)); + if (strpbrk(userName, quoting_relevant_chars)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("invalid character in extension owner: must not contain any of \"%s\"", + quoting_relevant_chars))); } /* @@ -1432,6 +1447,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, */ if (!control->relocatable) { + Datum old = t_sql; const char *qSchemaName = quote_identifier(schemaName); t_sql = DirectFunctionCall3Coll(replace_text, @@ -1439,6 +1455,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control, t_sql, CStringGetTextDatum("@extschema@"), CStringGetTextDatum(qSchemaName)); + if (t_sql != old && strpbrk(schemaName, quoting_relevant_chars)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), + errmsg("invalid character in extension \"%s\" schema: must not contain any of \"%s\"", + control->name, quoting_relevant_chars))); } /* diff --git a/test/expected/pg_tle_injection.out b/test/expected/pg_tle_injection.out index 8ab898a..93f9018 100644 --- a/test/expected/pg_tle_injection.out +++ b/test/expected/pg_tle_injection.out @@ -221,6 +221,81 @@ $_pg_tle_$ ); ERROR: invalid extension name: "test9.control"(),pg_sleep(10),pgtle."test9" DETAIL: Extension names must only contain alphanumeric characters or valid separators. +-- @extschema@ and @extowner@ substitutions are filtered through +-- quote_identifier(). A schema or owner name containing a character that +-- cannot be consistently quoted inside and outside of string literals (any of +-- " $ ' \) must be rejected rather than substituted into the script. +-- An extension whose script references @extschema@ cannot be created into a +-- schema whose name contains a quoting-relevant character. +SELECT pgtle.install_extension +( + 'ext_schema_subst', + '1.0', + 'references @extschema@', +$_pgtle_$ + CREATE FUNCTION whereami() RETURNS text AS $$ SELECT '@extschema@' $$ LANGUAGE SQL; +$_pgtle_$ +); + install_extension +------------------- + t +(1 row) + +CREATE SCHEMA "bad""schema"; +-- this should fail +CREATE EXTENSION ext_schema_subst SCHEMA "bad""schema"; +ERROR: invalid character in extension "ext_schema_subst" schema: must not contain any of ""$'\" +-- a schema with an ordinary name still works +CREATE SCHEMA good_schema; +CREATE EXTENSION ext_schema_subst SCHEMA good_schema; +SELECT good_schema.whereami(); + whereami +------------- + good_schema +(1 row) + +DROP EXTENSION ext_schema_subst; +SELECT pgtle.uninstall_extension('ext_schema_subst'); + uninstall_extension +--------------------- + t +(1 row) + +DROP SCHEMA "bad""schema"; +DROP SCHEMA good_schema; +-- An extension whose script references @extowner@ cannot be created by a role +-- whose name contains a quoting-relevant character. (The role is created as a +-- superuser only to avoid unrelated privilege setup; the substitution and its +-- validation run regardless of the caller's privileges.) +CREATE ROLE " owner'" SUPERUSER LOGIN; +SELECT pgtle.install_extension +( + 'ext_owner_subst', + '1.0', + 'references @extowner@', +$_pgtle_$ + CREATE FUNCTION owned_by() RETURNS text AS $$ SELECT '@extowner@' $$ LANGUAGE SQL; +$_pgtle_$ +); + install_extension +------------------- + t +(1 row) + +SET SESSION AUTHORIZATION " owner'"; +CREATE SCHEMA owner_schema; +-- this should fail +CREATE EXTENSION ext_owner_subst SCHEMA owner_schema; +ERROR: invalid character in extension owner: must not contain any of ""$'\" +RESET SESSION AUTHORIZATION; +SELECT pgtle.uninstall_extension('ext_owner_subst'); + uninstall_extension +--------------------- + t +(1 row) + +DROP SCHEMA owner_schema; +DROP ROLE " owner'"; -- cleanup DROP EXTENSION pg_tle; DROP SCHEMA pgtle; diff --git a/test/sql/pg_tle_injection.sql b/test/sql/pg_tle_injection.sql index 0b244d7..c61f940 100644 --- a/test/sql/pg_tle_injection.sql +++ b/test/sql/pg_tle_injection.sql @@ -175,6 +175,57 @@ $_pg_tle_$ $_pg_tle_$ ); +-- @extschema@ and @extowner@ substitutions are filtered through +-- quote_identifier(). A schema or owner name containing a character that +-- cannot be consistently quoted inside and outside of string literals (any of +-- " $ ' \) must be rejected rather than substituted into the script. + +-- An extension whose script references @extschema@ cannot be created into a +-- schema whose name contains a quoting-relevant character. +SELECT pgtle.install_extension +( + 'ext_schema_subst', + '1.0', + 'references @extschema@', +$_pgtle_$ + CREATE FUNCTION whereami() RETURNS text AS $$ SELECT '@extschema@' $$ LANGUAGE SQL; +$_pgtle_$ +); +CREATE SCHEMA "bad""schema"; +-- this should fail +CREATE EXTENSION ext_schema_subst SCHEMA "bad""schema"; +-- a schema with an ordinary name still works +CREATE SCHEMA good_schema; +CREATE EXTENSION ext_schema_subst SCHEMA good_schema; +SELECT good_schema.whereami(); +DROP EXTENSION ext_schema_subst; +SELECT pgtle.uninstall_extension('ext_schema_subst'); +DROP SCHEMA "bad""schema"; +DROP SCHEMA good_schema; + +-- An extension whose script references @extowner@ cannot be created by a role +-- whose name contains a quoting-relevant character. (The role is created as a +-- superuser only to avoid unrelated privilege setup; the substitution and its +-- validation run regardless of the caller's privileges.) +CREATE ROLE " owner'" SUPERUSER LOGIN; +SELECT pgtle.install_extension +( + 'ext_owner_subst', + '1.0', + 'references @extowner@', +$_pgtle_$ + CREATE FUNCTION owned_by() RETURNS text AS $$ SELECT '@extowner@' $$ LANGUAGE SQL; +$_pgtle_$ +); +SET SESSION AUTHORIZATION " owner'"; +CREATE SCHEMA owner_schema; +-- this should fail +CREATE EXTENSION ext_owner_subst SCHEMA owner_schema; +RESET SESSION AUTHORIZATION; +SELECT pgtle.uninstall_extension('ext_owner_subst'); +DROP SCHEMA owner_schema; +DROP ROLE " owner'"; + -- cleanup DROP EXTENSION pg_tle; DROP SCHEMA pgtle;