From 84d2229424233df7016ac0f6cf9c30426b59055f Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 3 Nov 2026 23:30:33 -0600 Subject: [PATCH] fix: attempt to change search_path guidance --- dockerfiles/docker-compose.yml | 1 + docs/0011_function_search_path_mutable.md | 8 +- lints/0011_function_search_path_mutable.sql | 136 ++++++++++++++---- splinter.sql | 136 ++++++++++++++---- .../0011_function_search_path_mutable.out | 22 ++- .../sql/0011_function_search_path_mutable.sql | 10 ++ 6 files changed, 252 insertions(+), 61 deletions(-) diff --git a/dockerfiles/docker-compose.yml b/dockerfiles/docker-compose.yml index 47c7278a..13e8a50b 100644 --- a/dockerfiles/docker-compose.yml +++ b/dockerfiles/docker-compose.yml @@ -23,6 +23,7 @@ services: - | ./bin/installcheck status=$$? + cat /home/splinter/regression.diffs cp /home/splinter/regression.diffs \ /home/splinter/regression.out \ /home/splinter/results/* \ diff --git a/docs/0011_function_search_path_mutable.md b/docs/0011_function_search_path_mutable.md index 30e388cb..3db53ab9 100644 --- a/docs/0011_function_search_path_mutable.md +++ b/docs/0011_function_search_path_mutable.md @@ -9,7 +9,7 @@ ### Rationale -In PostgreSQL, the `search_path` determines the order in which schemas are searched to find unqualified objects (like tables, functions, etc.). Setting `search_path` explicitly for a function is a best practice that ensures its behavior is consistent and secure, regardless of the executing user's default `search_path` settings. We recommend pinning functions' `search_path` to an empty string, `search_path = ''`, which forces all references within the function's body to be fully qualified. This helps prevent unexpected behavior due to changes in the `search_path` and mitigates potential security vulnerabilities. +In PostgreSQL, the `search_path` determines the order in which schemas are searched to find unqualified objects (like tables, functions, etc.). Setting `search_path` explicitly for a function is a best practice that ensures its behavior is consistent and secure, regardless of the executing user's default `search_path` settings. We recommend pinning functions' `search_path` to `search_path = 'pg_catalog, pg_temp'`, which forces all references within the function's body to be fully qualified, except `pg_catalog` and `pg_temp`. This helps prevent unexpected behavior due to changes in the `search_path` and mitigates potential security vulnerabilities. ### What is the Search Path? @@ -24,7 +24,7 @@ When a function does not have its `search_path` explicitly set, it inherits the ### How to Resolve -To ensure that your functions are secure and behave consistently, set the search path explicitly to an empty string within the function's definition. +To ensure that your functions are secure and behave consistently, set the search path explicitly to `pg_catalog, pg_temp` within the function's definition. Given a function like: @@ -43,10 +43,10 @@ You can `create or replace` the function and add the `search_path` setting. create or replace function example_function() returns void language sql - set search_path = '' -- LOOK HERE + set search_path = 'pg_catalog, pg_temp' -- LOOK HERE as $$ -- Your SQL code here. $$; ``` -Remember that once you set the `search_path = ''` all references to tables/functions/views/etc in your function's body must be qualified with a schema name. +Remember that once you set the `search_path = 'pg_catalog, pg_temp'` all references to tables/functions/views/etc in your function's body which are no in `pg_catalog` or `pg_temp`, must be qualified with a schema name. diff --git a/lints/0011_function_search_path_mutable.sql b/lints/0011_function_search_path_mutable.sql index 36ed1dbf..592fe81f 100644 --- a/lints/0011_function_search_path_mutable.sql +++ b/lints/0011_function_search_path_mutable.sql @@ -1,45 +1,127 @@ create view lint."0011_function_search_path_mutable" as +with security_definer_fn_settings as ( + select + n.nspname as schema_name, + p.proname as function_name, + p.prosrc as function_src, + + string_to_array( + unnest(p.proconfig), + '=' + ) as configuration_setting + from pg_proc as p + inner join pg_namespace as n on p.pronamespace = n.oid + union all + select + n.nspname as schema_name, + p.proname as function_name, + p.prosrc as function_src, + array[]::text [] as configuration_setting + from pg_proc as p + inner join pg_namespace as n on p.pronamespace = n.oid + where p.proconfig is null +), + +security_definer_fn_search_paths as ( + select + s.schema_name, + s.function_name, + s.function_src, + string_to_array(btrim( + (s.configuration_setting)[2], + '"' + ), + ',') as search_path_ary + from security_definer_fn_settings as s + where (s.configuration_setting)[1] = 'search_path' + union all + select + s.schema_name, + s.function_name, + s.function_src, + array[]::text [] as search_path_ary + from security_definer_fn_settings as s + where + (s.configuration_setting)[1] <> 'search_path' + or cardinality(s.configuration_setting) = 0 + +), + +security_definer_fn_search_paths_trimmed as ( + select + s.schema_name, + s.function_src, + s.function_name, + array( + select pg_catalog.btrim(x) + from unnest(s.search_path_ary) with ordinality as t (x, i) + order by i + ) as search_path_ary + from security_definer_fn_search_paths as s +) + select 'function_search_path_mutable' as name, 'Function Search Path Mutable' as title, 'WARN' as level, 'EXTERNAL' as facing, array['SECURITY'] as categories, - 'Detects functions where the search_path parameter is not set.' as description, + 'Detects functions where the search_path parameter is not set.' + as description, format( 'Function \`%s.%s\` has a role mutable search_path', - n.nspname, - p.proname + s.schema_name, + s.function_name ) as detail, - 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as remediation, + 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' + as remediation, jsonb_build_object( - 'schema', n.nspname, - 'name', p.proname, + 'schema', s.schema_name, + 'name', s.function_name, 'type', 'function' ) as metadata, format( 'function_search_path_mutable_%s_%s_%s', - n.nspname, - p.proname, - md5(p.prosrc) -- required when function is polymorphic + s.schema_name, + s.function_name, + md5(s.function_src) -- required when function is polymorphic ) as cache_key -from - pg_catalog.pg_proc p - join pg_catalog.pg_namespace n - on p.pronamespace = n.oid - left join pg_catalog.pg_depend dep - on p.oid = dep.objid - and dep.deptype = 'e' - and dep.classid = 'pg_catalog.pg_proc'::regclass -where - n.nspname not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' +from security_definer_fn_search_paths_trimmed as s +where s.schema_name not in ( + '_timescaledb_cache', + '_timescaledb_catalog', + '_timescaledb_config', + '_timescaledb_internal', + 'auth', + 'cron', + 'extensions', + 'graphql', + 'graphql_public', + 'information_schema', + 'net', + 'pgmq', + 'pgroonga', + 'pgsodium', + 'pgsodium_masks', + 'pgtle', + 'pgbouncer', + 'pg_catalog', + 'realtime', + 'repack', + 'storage', + 'supabas +e_functions', + 'supabase_migrations', + 'tiger', + 'topology', + 'vault' +) +and ( + ( + array_position(s.search_path_ary, 'pg_temp') + < array_position(s.search_path_ary, 'pg_catalog') ) - and dep.objid is null -- exclude functions owned by extensions - -- Search path not set - and not exists ( - select 1 - from unnest(coalesce(p.proconfig, '{}')) as config - where config like 'search_path=%' - ); + or array_position(s.search_path_ary, 'pg_temp') is null + or array_position(s.search_path_ary, 'pg_catalog') is null +); diff --git a/splinter.sql b/splinter.sql index 46bb331e..2d7a81c6 100644 --- a/splinter.sql +++ b/splinter.sql @@ -659,49 +659,131 @@ where )) union all ( +with security_definer_fn_settings as ( + select + n.nspname as schema_name, + p.proname as function_name, + p.prosrc as function_src, + + string_to_array( + unnest(p.proconfig), + '=' + ) as configuration_setting + from pg_proc as p + inner join pg_namespace as n on p.pronamespace = n.oid + union all + select + n.nspname as schema_name, + p.proname as function_name, + p.prosrc as function_src, + array[]::text [] as configuration_setting + from pg_proc as p + inner join pg_namespace as n on p.pronamespace = n.oid + where p.proconfig is null +), + +security_definer_fn_search_paths as ( + select + s.schema_name, + s.function_name, + s.function_src, + string_to_array(btrim( + (s.configuration_setting)[2], + '"' + ), + ',') as search_path_ary + from security_definer_fn_settings as s + where (s.configuration_setting)[1] = 'search_path' + union all + select + s.schema_name, + s.function_name, + s.function_src, + array[]::text [] as search_path_ary + from security_definer_fn_settings as s + where + (s.configuration_setting)[1] <> 'search_path' + or cardinality(s.configuration_setting) = 0 + +), + +security_definer_fn_search_paths_trimmed as ( + select + s.schema_name, + s.function_src, + s.function_name, + array( + select pg_catalog.btrim(x) + from unnest(s.search_path_ary) with ordinality as t (x, i) + order by i + ) as search_path_ary + from security_definer_fn_search_paths as s +) + select 'function_search_path_mutable' as name, 'Function Search Path Mutable' as title, 'WARN' as level, 'EXTERNAL' as facing, array['SECURITY'] as categories, - 'Detects functions where the search_path parameter is not set.' as description, + 'Detects functions where the search_path parameter is not set.' + as description, format( 'Function \`%s.%s\` has a role mutable search_path', - n.nspname, - p.proname + s.schema_name, + s.function_name ) as detail, - 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' as remediation, + 'https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable' + as remediation, jsonb_build_object( - 'schema', n.nspname, - 'name', p.proname, + 'schema', s.schema_name, + 'name', s.function_name, 'type', 'function' ) as metadata, format( 'function_search_path_mutable_%s_%s_%s', - n.nspname, - p.proname, - md5(p.prosrc) -- required when function is polymorphic + s.schema_name, + s.function_name, + md5(s.function_src) -- required when function is polymorphic ) as cache_key -from - pg_catalog.pg_proc p - join pg_catalog.pg_namespace n - on p.pronamespace = n.oid - left join pg_catalog.pg_depend dep - on p.oid = dep.objid - and dep.deptype = 'e' - and dep.classid = 'pg_catalog.pg_proc'::regclass -where - n.nspname not in ( - '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' +from security_definer_fn_search_paths_trimmed as s +where s.schema_name not in ( + '_timescaledb_cache', + '_timescaledb_catalog', + '_timescaledb_config', + '_timescaledb_internal', + 'auth', + 'cron', + 'extensions', + 'graphql', + 'graphql_public', + 'information_schema', + 'net', + 'pgmq', + 'pgroonga', + 'pgsodium', + 'pgsodium_masks', + 'pgtle', + 'pgbouncer', + 'pg_catalog', + 'realtime', + 'repack', + 'storage', + 'supabas +e_functions', + 'supabase_migrations', + 'tiger', + 'topology', + 'vault' +) +and ( + ( + array_position(s.search_path_ary, 'pg_temp') + < array_position(s.search_path_ary, 'pg_catalog') ) - and dep.objid is null -- exclude functions owned by extensions - -- Search path not set - and not exists ( - select 1 - from unnest(coalesce(p.proconfig, '{}')) as config - where config like 'search_path=%' - )) + or array_position(s.search_path_ary, 'pg_temp') is null + or array_position(s.search_path_ary, 'pg_catalog') is null +)) union all ( select diff --git a/test/expected/0011_function_search_path_mutable.out b/test/expected/0011_function_search_path_mutable.out index b24c5566..a81e334e 100644 --- a/test/expected/0011_function_search_path_mutable.out +++ b/test/expected/0011_function_search_path_mutable.out @@ -29,9 +29,10 @@ begin; $$; -- 1 issue select * from lint."0011_function_search_path_mutable"; - name | title | level | facing | categories | description | detail | remediation | metadata | cache_key -------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- -(0 rows) + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------------------------------+------------------------------+-------+----------+------------+---------------------------------------------------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------------------+---------------------------------------------------------+-------------------------------------------------------------------------- + function_search_path_mutable | Function Search Path Mutable | WARN | EXTERNAL | {SECURITY} | Detects functions where the search_path parameter is not set. | Function \`public.abc\` has a role mutable search_path | https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable | {"name": "abc", "type": "function", "schema": "public"} | function_search_path_mutable_public_abc_0c1ac0f6e8cc59f7cc75c5757c341521 +(1 row) -- Replace function and set search_path to non-empty string create or replace function public.abc() @@ -42,6 +43,21 @@ begin; select 1; $$; -- 1 issue + select * from lint."0011_function_search_path_mutable"; + name | title | level | facing | categories | description | detail | remediation | metadata | cache_key +------------------------------+------------------------------+-------+----------+------------+---------------------------------------------------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------------------+---------------------------------------------------------+-------------------------------------------------------------------------- + function_search_path_mutable | Function Search Path Mutable | WARN | EXTERNAL | {SECURITY} | Detects functions where the search_path parameter is not set. | Function \`public.abc\` has a role mutable search_path | https://supabase.com/docs/guides/database/database-linter?lint=0011_function_search_path_mutable | {"name": "abc", "type": "function", "schema": "public"} | function_search_path_mutable_public_abc_0c1ac0f6e8cc59f7cc75c5757c341521 +(1 row) + + -- Replace function and set search_path to 'pg_catalog, pg_temp' + create or replace function public.abc() + returns int + set search_path=pg_catalog, pg_temp + language sql + as $$ + select 1; + $$; + -- 0 issue select * from lint."0011_function_search_path_mutable"; name | title | level | facing | categories | description | detail | remediation | metadata | cache_key ------+-------+-------+--------+------------+-------------+--------+-------------+----------+----------- diff --git a/test/sql/0011_function_search_path_mutable.sql b/test/sql/0011_function_search_path_mutable.sql index a727fce2..e3ba42bb 100644 --- a/test/sql/0011_function_search_path_mutable.sql +++ b/test/sql/0011_function_search_path_mutable.sql @@ -38,6 +38,16 @@ begin; -- 1 issue select * from lint."0011_function_search_path_mutable"; + -- Replace function and set search_path to 'pg_catalog, pg_temp' + create or replace function public.abc() + returns int + set search_path=pg_catalog, pg_temp + language sql + as $$ + select 1; + $$; + -- 0 issue + select * from lint."0011_function_search_path_mutable"; rollback;