Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dockerfiles/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/* \
Expand Down
8 changes: 4 additions & 4 deletions docs/0011_function_search_path_mutable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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:

Expand All @@ -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.
136 changes: 109 additions & 27 deletions lints/0011_function_search_path_mutable.sql
Original file line number Diff line number Diff line change
@@ -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
);
136 changes: 109 additions & 27 deletions splinter.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions test/expected/0011_function_search_path_mutable.out
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
------+-------+-------+--------+------------+-------------+--------+-------------+----------+-----------
Expand Down
Loading
Loading