From c04aa31f6a35bc980d975ba6c43d486418efbc7a Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 30 Oct 2026 02:34:18 -0500 Subject: [PATCH 1/2] Put pg_temp last in SECURITY DEFINER search_path SET search_path = pg_catalog leaves pg_temp searched first for relations and types. List it last explicitly, in fresh installs and in the 0.3.3--stable update, and assert it for every SECURITY DEFINER function in the extension. --- HISTORY.asc | 3 +++ sql/trunklet--0.3.3--stable.sql | 8 ++++++++ sql/trunklet.sql | 14 +++++++------- test/core/functions.sql | 25 ++++++++++++++++++------- test/expected/all.out | 14 +++++++------- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/HISTORY.asc b/HISTORY.asc index 23a761b..a32b581 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -2,6 +2,9 @@ STABLE ------ === Adopt the `stable` pseudo-version between releases +=== Add `pg_temp` to the `search_path` of the `SECURITY DEFINER` functions +They now use `SET search_path = pg_catalog, pg_temp`, so `pg_temp` is searched last rather than first. Updated installs get this from `0.3.3--stable`. + === Add `0.2.0--0.2.1` update script `ALTER EXTENSION trunklet UPDATE` now works from 0.2.0. Previously there was no update path from 0.2.0. Updated installs now also match a fresh install: the `language__get()` debug message that a fresh 0.3.3 install has is added by `0.3.3--stable` and was retroactively added to the already-released `0.3.2--0.3.3` update script. diff --git a/sql/trunklet--0.3.3--stable.sql b/sql/trunklet--0.3.3--stable.sql index 3d345ef..756ec4d 100644 --- a/sql/trunklet--0.3.3--stable.sql +++ b/sql/trunklet--0.3.3--stable.sql @@ -19,3 +19,11 @@ EXCEPTION ; END $body$; + +ALTER FUNCTION trunklet.template__add(character varying, text, integer, text) SET search_path = pg_catalog, pg_temp; +ALTER FUNCTION trunklet.template__remove(integer) SET search_path = pg_catalog, pg_temp; +ALTER FUNCTION trunklet.template__remove(text, integer) SET search_path = pg_catalog, pg_temp; +ALTER FUNCTION trunklet.process_language(character varying, text, anyelement) SET search_path = pg_catalog, pg_temp; +ALTER FUNCTION trunklet.process(integer, anyelement) SET search_path = pg_catalog, pg_temp; +ALTER FUNCTION trunklet.process(text, integer, anyelement) SET search_path = pg_catalog, pg_temp; +ALTER FUNCTION trunklet.extract_parameters(character varying, anyelement, text[]) SET search_path = pg_catalog, pg_temp; diff --git a/sql/trunklet.sql b/sql/trunklet.sql index a56988a..c88b610 100644 --- a/sql/trunklet.sql +++ b/sql/trunklet.sql @@ -557,7 +557,7 @@ CREATE OR REPLACE FUNCTION trunklet.template__add( ) RETURNS _trunklet.template.template_id%TYPE -- !!!!!!! -SECURITY DEFINER SET search_path=pg_catalog +SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- !!!!!! LANGUAGE sql AS $body$ @@ -588,7 +588,7 @@ CREATE OR REPLACE FUNCTION trunklet.template__remove( ) RETURNS void -- !!!!!!! -SECURITY DEFINER SET search_path=pg_catalog +SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- !!!!!! LANGUAGE sql AS $body$ @@ -600,7 +600,7 @@ CREATE OR REPLACE FUNCTION trunklet.template__remove( ) RETURNS void -- !!!!!!! -SECURITY DEFINER SET search_path=pg_catalog +SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- !!!!!! LANGUAGE sql AS $body$ @@ -679,7 +679,7 @@ CREATE OR REPLACE FUNCTION trunklet.process_language( ) RETURNS text LANGUAGE plpgsql -- !!!!!!! -SECURITY DEFINER SET search_path = pg_catalog +SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- !!!!!!! AS $body$ @@ -720,7 +720,7 @@ CREATE OR REPLACE FUNCTION trunklet.process( , parameters anyelement ) RETURNS text LANGUAGE SQL -- !!! -SECURITY DEFINER SET search_path = pg_catalog +SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- !!! AS $body$ SELECT trunklet.process_language( @@ -737,7 +737,7 @@ CREATE OR REPLACE FUNCTION trunklet.process( , parameters anyelement ) RETURNS text LANGUAGE SQL -- !!! -SECURITY DEFINER SET search_path = pg_catalog +SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- !!! AS $body$ SELECT trunklet.process_language( @@ -902,7 +902,7 @@ CREATE OR REPLACE FUNCTION trunklet.extract_parameters( ) RETURNS anyelement LANGUAGE plpgsql -- !!!!!!! -SECURITY DEFINER SET search_path = pg_catalog +SECURITY DEFINER SET search_path = pg_catalog, pg_temp -- !!!!!!! AS $body$ diff --git a/test/core/functions.sql b/test/core/functions.sql index 76bb94f..43bec09 100644 --- a/test/core/functions.sql +++ b/test/core/functions.sql @@ -615,18 +615,29 @@ BEGIN ) a ; + -- pg_temp must be listed last, or it is searched first RETURN QUERY SELECT is( - proconfig - , '{search_path=pg_catalog}' - , 'Verify search_path for ' || full_name + ( + SELECT btrim((string_to_array(substr(c, length('search_path=') + 1), ','))[ + cardinality(string_to_array(substr(c, length('search_path=') + 1), ',')) + ]) + FROM unnest(proconfig) c + WHERE c LIKE 'search_path=%' + ) + , 'pg_temp' + , 'Verify search_path ends with pg_temp for ' || full_name ) -- This silliness is necessary to ensure stable ordering of the test FROM ( - SELECT *, p.oid::regprocedure AS full_name + SELECT p.proconfig, p.oid::regprocedure AS full_name FROM pg_proc p - JOIN pg_namespace n ON n.oid = p.pronamespace - WHERE nspname = 'trunklet' - AND prosecdef + JOIN pg_depend d + ON d.classid = 'pg_proc'::regclass + AND d.objid = p.oid + AND d.deptype = 'e' + AND d.refclassid = 'pg_extension'::regclass + AND d.refobjid = (SELECT oid FROM pg_extension WHERE extname = 'trunklet') + WHERE prosecdef -- Cast to text is necessary to avoid ordering by the actual OID in -- 9.5. COLLATE is necessary to ensure a consistent order. ORDER BY p.oid::regprocedure::text COLLATE "C" diff --git a/test/expected/all.out b/test/expected/all.out index 0294494..a495a98 100644 --- a/test/expected/all.out +++ b/test/expected/all.out @@ -108,13 +108,13 @@ ok 8 - _trunklet_test.test_schemas ok 5 - Verify acl for trunklet.template__add(character varying,text,integer,text) ok 6 - Verify acl for trunklet.template__remove(integer) ok 7 - Verify acl for trunklet.template__remove(text,integer) - ok 8 - Verify search_path for trunklet.extract_parameters(character varying,anyelement,text[]) - ok 9 - Verify search_path for trunklet.process(integer,anyelement) - ok 10 - Verify search_path for trunklet.process(text,integer,anyelement) - ok 11 - Verify search_path for trunklet.process_language(character varying,text,anyelement) - ok 12 - Verify search_path for trunklet.template__add(character varying,text,integer,text) - ok 13 - Verify search_path for trunklet.template__remove(integer) - ok 14 - Verify search_path for trunklet.template__remove(text,integer) + ok 8 - Verify search_path ends with pg_temp for trunklet.extract_parameters(character varying,anyelement,text[]) + ok 9 - Verify search_path ends with pg_temp for trunklet.process(integer,anyelement) + ok 10 - Verify search_path ends with pg_temp for trunklet.process(text,integer,anyelement) + ok 11 - Verify search_path ends with pg_temp for trunklet.process_language(character varying,text,anyelement) + ok 12 - Verify search_path ends with pg_temp for trunklet.template__add(character varying,text,integer,text) + ok 13 - Verify search_path ends with pg_temp for trunklet.template__remove(integer) + ok 14 - Verify search_path ends with pg_temp for trunklet.template__remove(text,integer) 1..14 ok 9 - _trunklet_test.test_secdef_privs # Subtest: _trunklet_test.test_template__add() From d55c4b2e8552409e951d379515140a00fd8f628e Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Fri, 30 Oct 2026 12:02:03 -0500 Subject: [PATCH 2/2] Assert the exact search_path of SECURITY DEFINER functions An ends-with-pg_temp check passes for 'public, pg_catalog, pg_temp'. Compare against the exact value, and comment the ALTER block in the 0.3.3--stable update script. --- sql/trunklet--0.3.3--stable.sql | 1 + test/core/functions.sql | 14 ++++---------- test/expected/all.out | 14 +++++++------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/sql/trunklet--0.3.3--stable.sql b/sql/trunklet--0.3.3--stable.sql index 756ec4d..29ef06d 100644 --- a/sql/trunklet--0.3.3--stable.sql +++ b/sql/trunklet--0.3.3--stable.sql @@ -20,6 +20,7 @@ EXCEPTION END $body$; +-- Match the SECURITY DEFINER search_path in a fresh install ALTER FUNCTION trunklet.template__add(character varying, text, integer, text) SET search_path = pg_catalog, pg_temp; ALTER FUNCTION trunklet.template__remove(integer) SET search_path = pg_catalog, pg_temp; ALTER FUNCTION trunklet.template__remove(text, integer) SET search_path = pg_catalog, pg_temp; diff --git a/test/core/functions.sql b/test/core/functions.sql index 43bec09..927f97c 100644 --- a/test/core/functions.sql +++ b/test/core/functions.sql @@ -615,17 +615,11 @@ BEGIN ) a ; - -- pg_temp must be listed last, or it is searched first + -- Exact value: pg_temp must come after pg_catalog or it can shadow it RETURN QUERY SELECT is( - ( - SELECT btrim((string_to_array(substr(c, length('search_path=') + 1), ','))[ - cardinality(string_to_array(substr(c, length('search_path=') + 1), ',')) - ]) - FROM unnest(proconfig) c - WHERE c LIKE 'search_path=%' - ) - , 'pg_temp' - , 'Verify search_path ends with pg_temp for ' || full_name + proconfig + , ARRAY['search_path=pg_catalog, pg_temp'] + , 'Verify search_path for ' || full_name ) -- This silliness is necessary to ensure stable ordering of the test FROM ( diff --git a/test/expected/all.out b/test/expected/all.out index a495a98..0294494 100644 --- a/test/expected/all.out +++ b/test/expected/all.out @@ -108,13 +108,13 @@ ok 8 - _trunklet_test.test_schemas ok 5 - Verify acl for trunklet.template__add(character varying,text,integer,text) ok 6 - Verify acl for trunklet.template__remove(integer) ok 7 - Verify acl for trunklet.template__remove(text,integer) - ok 8 - Verify search_path ends with pg_temp for trunklet.extract_parameters(character varying,anyelement,text[]) - ok 9 - Verify search_path ends with pg_temp for trunklet.process(integer,anyelement) - ok 10 - Verify search_path ends with pg_temp for trunklet.process(text,integer,anyelement) - ok 11 - Verify search_path ends with pg_temp for trunklet.process_language(character varying,text,anyelement) - ok 12 - Verify search_path ends with pg_temp for trunklet.template__add(character varying,text,integer,text) - ok 13 - Verify search_path ends with pg_temp for trunklet.template__remove(integer) - ok 14 - Verify search_path ends with pg_temp for trunklet.template__remove(text,integer) + ok 8 - Verify search_path for trunklet.extract_parameters(character varying,anyelement,text[]) + ok 9 - Verify search_path for trunklet.process(integer,anyelement) + ok 10 - Verify search_path for trunklet.process(text,integer,anyelement) + ok 11 - Verify search_path for trunklet.process_language(character varying,text,anyelement) + ok 12 - Verify search_path for trunklet.template__add(character varying,text,integer,text) + ok 13 - Verify search_path for trunklet.template__remove(integer) + ok 14 - Verify search_path for trunklet.template__remove(text,integer) 1..14 ok 9 - _trunklet_test.test_secdef_privs # Subtest: _trunklet_test.test_template__add()