Skip to content
Merged
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 ansible/files/postgresql_config/conf.d/pg_net.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pg_net.username = 'postgres'
3 changes: 2 additions & 1 deletion ansible/tasks/finalize-ami.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
group: 'postgres'
src: 'files/postgresql_config/postgresql-csvlog.conf'

- name: auto_explain and pg_cron confs
- name: auto_explain, pg_cron, and pg_net confs
ansible.builtin.template:
dest: "/etc/postgresql-custom/conf.d/{{ ext_item }}.conf"
group: 'postgres'
src: "files/postgresql_config/conf.d/{{ ext_item }}.conf"
loop:
- auto_explain
- pg_cron
- pg_net
loop_control:
loop_var: 'ext_item'

Expand Down
1 change: 1 addition & 0 deletions docker/pgctld/orioledb-postgresql.conf.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ include = '/etc/postgresql-custom/supautils.conf'
# Add settings for extensions here
auto_explain.log_min_duration = 10s
cron.database_name = 'postgres'
pg_net.username = 'postgres'
pgsodium.getkey_script = '/usr/lib/postgresql/bin/pgsodium_getkey.sh'
vault.getkey_script = '/usr/lib/postgresql/bin/pgsodium_getkey.sh'
wal_log_hints = 'on'
Expand Down
1 change: 1 addition & 0 deletions docker/pgctld/postgresql.conf.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ include = '/etc/postgresql-custom/supautils.conf'
# Add settings for extensions here
auto_explain.log_min_duration = 10s
cron.database_name = 'postgres'
pg_net.username = 'postgres'
pgsodium.getkey_script = '/usr/lib/postgresql/bin/pgsodium_getkey.sh'
vault.getkey_script = '/usr/lib/postgresql/bin/pgsodium_getkey.sh'
wal_log_hints = 'on'
1 change: 1 addition & 0 deletions nix/checks.nix
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@
"security" # depends on various extensions
"extensions_schema" # tests extension loading
"roles" # includes roles/schemas from extensions not in CLI (pgtle, pgmq, repack, topology)
"pg_net_worker_privileges" # needs the authenticated/postgres roles from the full migrations, not present in the CLI prime file
# Version-specific extension tests
"z_17_ext_interface"
"z_17_pg_stat_monitor"
Expand Down
75 changes: 75 additions & 0 deletions nix/tests/expected/pg_net_worker_privileges.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
-- Regression test for a privilege-escalation vulnerability in pg_net's background
-- worker. See ansible/files/postgresql_config/conf.d/pg_net.conf, which sets
-- pg_net.username = 'postgres' to fix it.
--
-- pg_net.username controls which role the background worker uses to drain
-- net.http_request_queue and write to net._http_response. When unset, the worker
-- connects as the bootstrap superuser -- supabase_admin on this platform, a true
-- superuser.
--
-- pg_net's own install script grants ALL privileges (which includes TRIGGER) on
-- both of those tables to PUBLIC:
-- grant all on all tables in schema net to PUBLIC;
-- so literally any role -- including a `postgres` app user -- can attach
-- a trigger to them. Since a normal (non SECURITY DEFINER) trigger runs with the
-- privileges of whoever performs the triggering statement, a `postgres` user
-- could make such a trigger run arbitrary SQL with the pg_net worker's privileges
-- the next time it processed a queued request.
--
-- This test plants such a trigger as `postgres` and has it record which role
-- and privilege level it actually ran with, confirming the worker's DML now runs
-- as the unprivileged `postgres` role instead of the superuser `supabase_admin`.
create table public.pg_net_privesc_log (
recorded_role name,
recorded_is_superuser bool
);
grant insert on public.pg_net_privesc_log to public;
create function public.pg_net_privesc_trigger() returns trigger
language plpgsql
security invoker
as $$
begin
-- records the privileges of whoever actually performed the triggering
-- statement -- i.e. the pg_net worker, not whoever created this trigger
insert into public.pg_net_privesc_log (recorded_role, recorded_is_superuser)
values (current_user, (select rolsuper from pg_roles where rolname = current_user));
return null;
end;
$$;
grant execute on function public.pg_net_privesc_trigger() to public;
set session authorization postgres;
create trigger pg_net_privesc
after insert on net._http_response
for each row execute function public.pg_net_privesc_trigger();
select net.http_get(
'http://localhost:' || (select value from test_config where key = 'http_mock_port') || '/get'
) is not null as request_queued;
request_queued
----------------
t
(1 row)

reset session authorization;
-- give the background worker time to drain the queue and process the response
do $$
declare
i int := 0;
begin
while (select count(*) from public.pg_net_privesc_log) = 0 and i < 40 loop
perform pg_sleep(0.25);
i := i + 1;
end loop;
end;
$$;
-- the pg_net worker must have run the trigger as the unprivileged `postgres`
-- role, not the superuser `supabase_admin`
select recorded_role, recorded_is_superuser from public.pg_net_privesc_log;
recorded_role | recorded_is_superuser
---------------+-----------------------
postgres | f
(1 row)

-- cleanup
drop trigger if exists pg_net_privesc on net._http_response;
drop function if exists public.pg_net_privesc_trigger();
drop table public.pg_net_privesc_log;
1 change: 1 addition & 0 deletions nix/tests/postgresql.conf.in
Original file line number Diff line number Diff line change
Expand Up @@ -799,3 +799,4 @@ vault.getkey_script = '@PGSODIUM_GETKEY_SCRIPT@'

auto_explain.log_min_duration = 10s
cron.database_name = 'postgres'
pg_net.username = 'postgres'
76 changes: 76 additions & 0 deletions nix/tests/sql/pg_net_worker_privileges.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
-- Regression test for a privilege-escalation vulnerability in pg_net's background
-- worker. See ansible/files/postgresql_config/conf.d/pg_net.conf, which sets
-- pg_net.username = 'postgres' to fix it.
--
-- pg_net.username controls which role the background worker uses to drain
-- net.http_request_queue and write to net._http_response. When unset, the worker
-- connects as the bootstrap superuser -- supabase_admin on this platform, a true
-- superuser.
--
-- pg_net's own install script grants ALL privileges (which includes TRIGGER) on
-- both of those tables to PUBLIC:
-- grant all on all tables in schema net to PUBLIC;
-- so literally any role -- including a `postgres` app user -- can attach
-- a trigger to them. Since a normal (non SECURITY DEFINER) trigger runs with the
-- privileges of whoever performs the triggering statement, a `postgres` user
-- could make such a trigger run arbitrary SQL with the pg_net worker's privileges
-- the next time it processed a queued request.
--
-- This test plants such a trigger as `postgres` and has it record which role
-- and privilege level it actually ran with, confirming the worker's DML now runs
-- as the unprivileged `postgres` role instead of the superuser `supabase_admin`.

Comment thread
imor marked this conversation as resolved.
create table public.pg_net_privesc_log (
recorded_role name,
recorded_is_superuser bool
);

grant insert on public.pg_net_privesc_log to public;

create function public.pg_net_privesc_trigger() returns trigger
language plpgsql
security invoker
as $$
begin
-- records the privileges of whoever actually performed the triggering
-- statement -- i.e. the pg_net worker, not whoever created this trigger
insert into public.pg_net_privesc_log (recorded_role, recorded_is_superuser)
values (current_user, (select rolsuper from pg_roles where rolname = current_user));
return null;
end;
$$;

grant execute on function public.pg_net_privesc_trigger() to public;

set session authorization postgres;

create trigger pg_net_privesc
after insert on net._http_response
for each row execute function public.pg_net_privesc_trigger();

select net.http_get(
'http://localhost:' || (select value from test_config where key = 'http_mock_port') || '/get'
) is not null as request_queued;

reset session authorization;

-- give the background worker time to drain the queue and process the response
do $$
declare
i int := 0;
begin
while (select count(*) from public.pg_net_privesc_log) = 0 and i < 40 loop
perform pg_sleep(0.25);
i := i + 1;
end loop;
end;
$$;

-- the pg_net worker must have run the trigger as the unprivileged `postgres`
-- role, not the superuser `supabase_admin`
select recorded_role, recorded_is_superuser from public.pg_net_privesc_log;

-- cleanup
drop trigger if exists pg_net_privesc on net._http_response;
drop function if exists public.pg_net_privesc_trigger();
drop table public.pg_net_privesc_log;
Loading