From aca4001c8d79b6b298ab77832ead35e5ca9c3658 Mon Sep 17 00:00:00 2001 From: Jim Jones Date: Mon, 23 Mar 2026 14:20:24 +0100 Subject: [PATCH v13] Prevent superusers from accessing temp tables of other sessions --- src/backend/catalog/aclchk.c | 40 +++++ src/test/modules/test_misc/meson.build | 1 + .../test_misc/t/011_temp_obj_multisession.pl | 142 ++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 src/test/modules/test_misc/t/011_temp_obj_multisession.pl diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 67424fe3b0..7ffcfce8d2 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -48,6 +48,7 @@ #include "catalog/catalog.h" #include "catalog/dependency.h" #include "catalog/indexing.h" +#include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_authid.h" #include "catalog/pg_class.h" @@ -3350,6 +3351,21 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask, !superuser_arg(roleid)) mask &= ~(ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE); + /* + * Enforce temporary table isolation: prevent access to other sessions' + * temporary tables, even for superusers. + */ + if (classForm->relpersistence == RELPERSISTENCE_TEMP && + isOtherTempNamespace(classForm->relnamespace)) + { + ReleaseSysCache(tuple); + if (is_missing != NULL) + return 0; /* no privileges, but don't treat as missing */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot access temporary relations of other sessions"))); + } + /* * Otherwise, superusers bypass all permission-checking. */ @@ -4135,6 +4151,30 @@ object_ownercheck(Oid classid, Oid objectid, Oid roleid) SysCacheIdentifier cacheid; Oid ownerId; + /* + * For relations, block access to other sessions' temporary tables before + * checking superuser status. Temp tables are session-private by design; + * superuser access would allow cross-session data leakage. + */ + if (classid == RelationRelationId) + { + HeapTuple tup; + Form_pg_class relform; + + tup = SearchSysCache1(RELOID, ObjectIdGetDatum(objectid)); + if (HeapTupleIsValid(tup)) + { + relform = (Form_pg_class) GETSTRUCT(tup); + if (relform->relpersistence == RELPERSISTENCE_TEMP && + isOtherTempNamespace(relform->relnamespace)) + { + ReleaseSysCache(tup); + return false; + } + ReleaseSysCache(tup); + } + } + /* Superusers bypass all permission checking. */ if (superuser_arg(roleid)) return true; diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build index 6e8db1621a..356121673a 100644 --- a/src/test/modules/test_misc/meson.build +++ b/src/test/modules/test_misc/meson.build @@ -19,6 +19,7 @@ tests += { 't/008_replslot_single_user.pl', 't/009_log_temp_files.pl', 't/010_index_concurrently_upsert.pl', + 't/011_temp_obj_multisession.pl', ], # The injection points are cluster-wide, so disable installcheck 'runningcheck': false, diff --git a/src/test/modules/test_misc/t/011_temp_obj_multisession.pl b/src/test/modules/test_misc/t/011_temp_obj_multisession.pl new file mode 100644 index 0000000000..d1b3dbc1e0 --- /dev/null +++ b/src/test/modules/test_misc/t/011_temp_obj_multisession.pl @@ -0,0 +1,142 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::BackgroundPsql; +use Test::More; + +# Set up a fresh node +my $node = PostgreSQL::Test::Cluster->new('temp_lock'); +$node->init; +$node->start; + +# Create a long-lived session +my $psql1 = $node->background_psql('postgres'); + +$psql1->query_safe( + q(CREATE TEMP TABLE foo AS SELECT 42 AS val;)); + +my $tempschema = $node->safe_psql( + 'postgres', + q{ + SELECT n.nspname + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE relname = 'foo' AND relpersistence = 't'; + } +); +chomp $tempschema; +ok($tempschema =~ /^pg_temp_\d+$/, "got temp schema: $tempschema"); + + +# SELECT TEMPORARY TABLE from other session +my ($stdout, $stderr); +$node->psql( + 'postgres', + "SELECT val FROM $tempschema.foo;", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'SELECT on other session temp table is not allowed'); + +# UPDATE TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "UPDATE $tempschema.foo SET val = NULL;", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'UPDATE on other session temp table is not allowed'); + +# DELETE records from TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "DELETE FROM $tempschema.foo;", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'DELETE on other session temp table is not allowed'); + +# TRUNCATE TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "TRUNCATE TABLE $tempschema.foo;", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'TRUNCATE on other session temp table is not allowed'); + +# INSERT INTO TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "INSERT INTO $tempschema.foo VALUES (73);", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'INSERT INTO on other session temp table is not allowed'); + +# ALTER TABLE .. RENAME TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "ALTER TABLE $tempschema.foo RENAME TO bar;", + stderr => \$stderr +); +like($stderr, qr/must be owner of table foo/, + 'ALTER TABLE ... RENAME on other session temp table is blocked'); + +# ALTER TABLE .. ADD COLUMN in TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "ALTER TABLE $tempschema.foo ADD COLUMN bar int;", + stderr => \$stderr +); +like($stderr, qr/must be owner of table foo/, + 'ALTER TABLE ... ADD COLUMN on other session temp table is blocked'); + +# COPY TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "COPY $tempschema.foo TO '/tmp/x';", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'COPY on other session temp table is blocked'); + +# LOCK TEMPORARY TABLE from other session +$node->psql( + 'postgres', + "BEGIN; LOCK TABLE $tempschema.foo IN ACCESS EXCLUSIVE MODE;", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'LOCK on other session temp table is blocked'); + +# Access via search_path manipulation (unqualified name) +$node->psql( + 'postgres', + "SET search_path = $tempschema, public; SELECT val FROM foo;", + stderr => \$stderr +); +like($stderr, qr/cannot access temporary relations of other sessions/, + 'SELECT via search_path manipulation on other session temp table is blocked'); + +# has_table_privilege() should return false +my $result = $node->safe_psql( + 'postgres', + "SELECT has_table_privilege('$tempschema.foo'::regclass, 'SELECT');" +); +is($result, 'f', 'has_table_privilege returns false for other session temp table'); + +# DROP TEMPORARY TABLE from other session +my $ok = $node->psql( + 'postgres', + "DROP TABLE $tempschema.foo;" +); +ok($ok == 0, 'DROP TABLE executed successfully'); + +# Clean up +$psql1->quit; + +done_testing(); -- 2.43.0