From 808cdd98820335d58b7a15d3eb4d19455282766b Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Thu, 20 Aug 2026 13:05:30 +0200 Subject: [PATCH 1/4] chore: set search path in migration --- ...0001_rescope_pg_graphql_access_trigger.sql | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql diff --git a/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql b/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql new file mode 100644 index 000000000..ce9b5349f --- /dev/null +++ b/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql @@ -0,0 +1,48 @@ +-- migrate:up + +create or replace function extensions.grant_pg_graphql_access() + returns event_trigger + language plpgsql + set search_path to '' +as $func$ +begin + if not exists ( + select 1 + from pg_catalog.pg_event_trigger_ddl_commands() ev + join pg_catalog.pg_extension e on ev.objid = e.oid + where e.extname = 'pg_graphql' + ) then + return; + end if; + + drop function if exists graphql_public.graphql; + create or replace function graphql_public.graphql( + "operationName" text default null, + query text default null, + variables jsonb default null, + extensions jsonb default null + ) + returns jsonb + language sql + set search_path to '' + as $$ + select graphql.resolve( + query := query, + variables := coalesce(variables, '{}'), + "operationName" := "operationName", + extensions := extensions + ); + $$; + + -- Attach the wrapper to the extension so DROP EXTENSION cascades to it, + -- which in turn triggers set_graphql_placeholder to reinstall the "not enabled" stub. + alter extension pg_graphql add function graphql_public.graphql(text, text, jsonb, jsonb); + + grant usage on schema graphql to postgres, anon, authenticated, service_role; + grant execute on function graphql.resolve to postgres, anon, authenticated, service_role; + grant usage on schema graphql to postgres with grant option; + grant usage on schema graphql_public to postgres with grant option; +end; +$func$; + +-- migrate:down From 4e7bf35925139d842d9ebb53150fe4be1bc6227d Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Tue, 25 Aug 2026 15:30:56 +0200 Subject: [PATCH 2/4] chore: defend additional functions --- ...0001_rescope_pg_graphql_access_trigger.sql | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql b/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql index ce9b5349f..c9173d2e2 100644 --- a/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql +++ b/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql @@ -45,4 +45,195 @@ begin end; $func$; + +CREATE OR REPLACE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger + LANGUAGE plpgsql + SET search_path to '' + AS $$ +BEGIN + IF EXISTS ( + SELECT + FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext + ON ev.objid = ext.oid + WHERE ext.extname = 'pg_cron' + ) + THEN + grant usage on schema cron to postgres with grant option; + + alter default privileges in schema cron grant all on tables to postgres with grant option; + alter default privileges in schema cron grant all on functions to postgres with grant option; + alter default privileges in schema cron grant all on sequences to postgres with grant option; + + alter default privileges for user supabase_admin in schema cron grant all + on sequences to postgres with grant option; + alter default privileges for user supabase_admin in schema cron grant all + on tables to postgres with grant option; + alter default privileges for user supabase_admin in schema cron grant all + on functions to postgres with grant option; + + grant all privileges on all tables in schema cron to postgres with grant option; + revoke all on table cron.job from postgres; + grant select on table cron.job to postgres with grant option; + revoke trigger on cron.job_run_details from postgres; + END IF; +END; +$$; + +CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger + LANGUAGE plpgsql + SET search_path to '' + AS $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext + ON ev.objid = ext.oid + WHERE ext.extname = 'pg_net' + ) + THEN + IF NOT EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_functions_admin' + ) + THEN + CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; + END IF; + + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + + IF EXISTS ( + SELECT FROM pg_extension + WHERE extname = 'pg_net' + -- all versions in use on existing projects as of 2025-02-20 + -- version 0.12.0 onwards don't need these applied + AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8.0', '0.10.0', '0.11.0') + ) THEN + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END IF; +END; +$$; + +CREATE OR REPLACE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger + LANGUAGE plpgsql + SET search_path TO '' + AS $$ +DECLARE + cmd record; +BEGIN + FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands() + LOOP + IF cmd.command_tag IN ( + 'CREATE SCHEMA', 'ALTER SCHEMA' + , 'CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO', 'ALTER TABLE' + , 'CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE' + , 'CREATE VIEW', 'ALTER VIEW' + , 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW' + , 'CREATE FUNCTION', 'ALTER FUNCTION' + , 'CREATE TRIGGER' + , 'CREATE TYPE', 'ALTER TYPE' + , 'CREATE RULE' + , 'COMMENT' + ) + -- don't notify in case of CREATE TEMP table or other objects created on pg_temp + AND cmd.schema_name is distinct from 'pg_temp' + THEN + NOTIFY pgrst, 'reload schema'; + END IF; + END LOOP; +END; $$; + +CREATE OR REPLACE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger + LANGUAGE plpgsql + SET search_path TO '' + AS $$ +DECLARE + obj record; +BEGIN + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + LOOP + IF obj.object_type IN ( + 'schema' + , 'table' + , 'foreign table' + , 'view' + , 'materialized view' + , 'function' + , 'trigger' + , 'type' + , 'rule' + ) + AND obj.is_temporary IS false -- no pg_temp objects + THEN + NOTIFY pgrst, 'reload schema'; + END IF; + END LOOP; +END; $$; + +CREATE OR REPLACE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger + LANGUAGE plpgsql + SET search_path TO '' + AS $_$ + DECLARE + graphql_is_dropped bool; + BEGIN + graphql_is_dropped = ( + SELECT ev.schema_name = 'graphql_public' + FROM pg_event_trigger_dropped_objects() AS ev + WHERE ev.schema_name = 'graphql_public' + ); + + IF graphql_is_dropped + THEN + create or replace function graphql_public.graphql( + "operationName" text default null, + query text default null, + variables jsonb default null, + extensions jsonb default null + ) + returns jsonb + language plpgsql + set search_path to '' + as $$ + DECLARE + server_version float; + BEGIN + server_version = (SELECT (SPLIT_PART((select version()), ' ', 2))::float); + + IF server_version >= 14 THEN + RETURN jsonb_build_object( + 'errors', jsonb_build_array( + jsonb_build_object( + 'message', 'pg_graphql extension is not enabled.' + ) + ) + ); + ELSE + RETURN jsonb_build_object( + 'errors', jsonb_build_array( + jsonb_build_object( + 'message', 'pg_graphql is only available on projects running Postgres 14 onwards.' + ) + ) + ); + END IF; + END; + $$; + END IF; + + END; +$_$; -- migrate:down From 0f2ad99e6c046b88c469da6e2bf3ff65ac8579eb Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Mon, 31 Aug 2026 15:28:43 +0200 Subject: [PATCH 3/4] fix: regress tests --- .../20260820000001_rescope_pg_graphql_access_trigger.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql b/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql index c9173d2e2..9b8c7ea54 100644 --- a/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql +++ b/migrations/db/migrations/20260820000001_rescope_pg_graphql_access_trigger.sql @@ -75,7 +75,7 @@ BEGIN grant all privileges on all tables in schema cron to postgres with grant option; revoke all on table cron.job from postgres; grant select on table cron.job to postgres with grant option; - revoke trigger on cron.job_run_details from postgres; + revoke trigger on cron.job_run_details from postgres cascade; END IF; END; $$; From 8d21706e98836080ce3e8ffabe89a0567f196d5c Mon Sep 17 00:00:00 2001 From: Etienne Stalmans Date: Mon, 31 Aug 2026 15:57:54 +0200 Subject: [PATCH 4/4] chore: update base schemas --- migrations/schema-15.sql | 12 ++++++++++-- migrations/schema-17.sql | 12 ++++++++++-- migrations/schema-orioledb-17.sql | 12 ++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/migrations/schema-15.sql b/migrations/schema-15.sql index 322f47404..caa9755c1 100644 --- a/migrations/schema-15.sql +++ b/migrations/schema-15.sql @@ -169,6 +169,7 @@ $$; CREATE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ BEGIN IF EXISTS ( @@ -214,11 +215,12 @@ COMMENT ON FUNCTION extensions.grant_pg_cron_access() IS 'Grants access to pg_cr CREATE FUNCTION extensions.grant_pg_graphql_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $_$ begin if not exists ( select 1 - from pg_event_trigger_ddl_commands() ev + from pg_catalog.pg_event_trigger_ddl_commands() ev join pg_catalog.pg_extension e on ev.objid = e.oid where e.extname = 'pg_graphql' ) then @@ -234,6 +236,7 @@ begin ) returns jsonb language sql + set search_path to '' as $$ select graphql.resolve( query := query, @@ -268,6 +271,7 @@ COMMENT ON FUNCTION extensions.grant_pg_graphql_access() IS 'Grants access to pg CREATE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ BEGIN IF EXISTS ( @@ -294,7 +298,7 @@ BEGIN WHERE extname = 'pg_net' -- all versions in use on existing projects as of 2025-02-20 -- version 0.12.0 onwards don't need these applied - AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8', '0.10.0', '0.11.0') + AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8.0', '0.10.0', '0.11.0') ) THEN ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; @@ -326,6 +330,7 @@ COMMENT ON FUNCTION extensions.grant_pg_net_access() IS 'Grants access to pg_net CREATE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ DECLARE cmd record; @@ -359,6 +364,7 @@ END; $$; CREATE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ DECLARE obj record; @@ -390,6 +396,7 @@ END; $$; CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $_$ DECLARE graphql_is_dropped bool; @@ -410,6 +417,7 @@ CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger ) returns jsonb language plpgsql + set search_path to '' as $$ DECLARE server_version float; diff --git a/migrations/schema-17.sql b/migrations/schema-17.sql index 2cbfa1b14..bb9fa0c22 100644 --- a/migrations/schema-17.sql +++ b/migrations/schema-17.sql @@ -170,6 +170,7 @@ $$; CREATE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ BEGIN IF EXISTS ( @@ -215,11 +216,12 @@ COMMENT ON FUNCTION extensions.grant_pg_cron_access() IS 'Grants access to pg_cr CREATE FUNCTION extensions.grant_pg_graphql_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $_$ begin if not exists ( select 1 - from pg_event_trigger_ddl_commands() ev + from pg_catalog.pg_event_trigger_ddl_commands() ev join pg_catalog.pg_extension e on ev.objid = e.oid where e.extname = 'pg_graphql' ) then @@ -235,6 +237,7 @@ begin ) returns jsonb language sql + set search_path to '' as $$ select graphql.resolve( query := query, @@ -269,6 +272,7 @@ COMMENT ON FUNCTION extensions.grant_pg_graphql_access() IS 'Grants access to pg CREATE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ BEGIN IF EXISTS ( @@ -295,7 +299,7 @@ BEGIN WHERE extname = 'pg_net' -- all versions in use on existing projects as of 2025-02-20 -- version 0.12.0 onwards don't need these applied - AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8', '0.10.0', '0.11.0') + AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8.0', '0.10.0', '0.11.0') ) THEN ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; @@ -327,6 +331,7 @@ COMMENT ON FUNCTION extensions.grant_pg_net_access() IS 'Grants access to pg_net CREATE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ DECLARE cmd record; @@ -360,6 +365,7 @@ END; $$; CREATE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ DECLARE obj record; @@ -391,6 +397,7 @@ END; $$; CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $_$ DECLARE graphql_is_dropped bool; @@ -411,6 +418,7 @@ CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger ) returns jsonb language plpgsql + set search_path to '' as $$ DECLARE server_version float; diff --git a/migrations/schema-orioledb-17.sql b/migrations/schema-orioledb-17.sql index c1617fdc6..7f7afe6dc 100644 --- a/migrations/schema-orioledb-17.sql +++ b/migrations/schema-orioledb-17.sql @@ -184,6 +184,7 @@ $$; CREATE FUNCTION extensions.grant_pg_cron_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ BEGIN IF EXISTS ( @@ -229,11 +230,12 @@ COMMENT ON FUNCTION extensions.grant_pg_cron_access() IS 'Grants access to pg_cr CREATE FUNCTION extensions.grant_pg_graphql_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $_$ begin if not exists ( select 1 - from pg_event_trigger_ddl_commands() ev + from pg_catalog.pg_event_trigger_ddl_commands() ev join pg_catalog.pg_extension e on ev.objid = e.oid where e.extname = 'pg_graphql' ) then @@ -249,6 +251,7 @@ begin ) returns jsonb language sql + set search_path to '' as $$ select graphql.resolve( query := query, @@ -283,6 +286,7 @@ COMMENT ON FUNCTION extensions.grant_pg_graphql_access() IS 'Grants access to pg CREATE FUNCTION extensions.grant_pg_net_access() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ BEGIN IF EXISTS ( @@ -309,7 +313,7 @@ BEGIN WHERE extname = 'pg_net' -- all versions in use on existing projects as of 2025-02-20 -- version 0.12.0 onwards don't need these applied - AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8', '0.10.0', '0.11.0') + AND extversion IN ('0.2', '0.6', '0.7', '0.7.1', '0.8.0', '0.10.0', '0.11.0') ) THEN ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; @@ -341,6 +345,7 @@ COMMENT ON FUNCTION extensions.grant_pg_net_access() IS 'Grants access to pg_net CREATE FUNCTION extensions.pgrst_ddl_watch() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ DECLARE cmd record; @@ -374,6 +379,7 @@ END; $$; CREATE FUNCTION extensions.pgrst_drop_watch() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $$ DECLARE obj record; @@ -405,6 +411,7 @@ END; $$; CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger LANGUAGE plpgsql + SET search_path TO '' AS $_$ DECLARE graphql_is_dropped bool; @@ -425,6 +432,7 @@ CREATE FUNCTION extensions.set_graphql_placeholder() RETURNS event_trigger ) returns jsonb language plpgsql + set search_path to '' as $$ DECLARE server_version float;