From 8a2530ebcdef1aafa08ad1d019aec298dcebb952 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Mon, 10 Nov 2025 09:00:00 -0600 Subject: [PATCH] Check for CREATE privilege on the schema in CREATE STATISTICS. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This omission allowed table owners to create statistics in any schema, potentially leading to unexpected naming conflicts. For ALTER TABLE commands that require re-creating statistics objects, skip this check in case the user has since lost CREATE on the schema. The addition of a second parameter to CreateStatistics() breaks ABI compatibility, but we are unaware of any impacted third-party code. Reported-by: Jelte Fennema-Nio Author: Jelte Fennema-Nio Co-authored-by: Nathan Bossart Reviewed-by: Noah Misch Reviewed-by: Álvaro Herrera Security: CVE-2025-12817 Backpatch-through: 13 --- src/backend/commands/statscmds.c | 8 ++++++++ src/test/regress/expected/stats_ext.out | 14 ++++++++++++++ src/test/regress/sql/stats_ext.sql | 13 +++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index f3a6c2e7cc0..b524a08ec7e 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -33,6 +33,7 @@ #include "utils/builtins.h" #include "utils/fmgroids.h" #include "utils/inval.h" +#include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" #include "utils/syscache.h" @@ -89,6 +90,7 @@ CreateStatistics(CreateStatsStmt *stmt) bool requested_type = false; int i; ListCell *cell; + AclResult aclresult; Assert(IsA(stmt, CreateStatsStmt)); @@ -167,6 +169,12 @@ CreateStatistics(CreateStatsStmt *stmt) } namestrcpy(&stxname, namestr); + /* Check we have creation rights in target namespace. */ + aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_SCHEMA, + get_namespace_name(namespaceId)); + /* * Deal with the possibility that the statistics object already exists. */ diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out index fa06298504e..b5b3e5dc81c 100644 --- a/src/test/regress/expected/stats_ext.out +++ b/src/test/regress/expected/stats_ext.out @@ -1745,6 +1745,18 @@ SELECT * FROM tststats.priv_test_parent_tbl t (0 rows) DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak +-- CREATE STATISTICS checks for CREATE on the schema +RESET SESSION AUTHORIZATION; +CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT); +GRANT USAGE ON SCHEMA sts_sch1 TO regress_stats_user1; +ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS sts_sch1.fail ON a, b FROM sts_sch1.tbl; +ERROR: permission denied for schema sts_sch1 +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS sts_sch1.pass ON a, b FROM sts_sch1.tbl; -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); @@ -1756,4 +1768,6 @@ NOTICE: drop cascades to 3 other objects DETAIL: drop cascades to table tststats.priv_test_parent_tbl drop cascades to table tststats.priv_test_tbl drop cascades to view tststats.priv_test_view +DROP SCHEMA sts_sch1 CASCADE; +NOTICE: drop cascades to table sts_sch1.tbl DROP USER regress_stats_user1; diff --git a/src/test/regress/sql/stats_ext.sql b/src/test/regress/sql/stats_ext.sql index dd632f894e5..72e6489bbb7 100644 --- a/src/test/regress/sql/stats_ext.sql +++ b/src/test/regress/sql/stats_ext.sql @@ -963,6 +963,18 @@ SELECT * FROM tststats.priv_test_parent_tbl t WHERE a <<< 0 AND (b <<< 0 OR t.* <<< (1, 1) IS NOT NULL); -- Should not leak DELETE FROM tststats.priv_test_parent_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak +-- CREATE STATISTICS checks for CREATE on the schema +RESET SESSION AUTHORIZATION; +CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT); +GRANT USAGE ON SCHEMA sts_sch1 TO regress_stats_user1; +ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS sts_sch1.fail ON a, b FROM sts_sch1.tbl; +RESET SESSION AUTHORIZATION; +GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +CREATE STATISTICS sts_sch1.pass ON a, b FROM sts_sch1.tbl; + -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); @@ -970,4 +982,5 @@ DROP OPERATOR <<< (record, record); DROP FUNCTION op_leak(record, record); RESET SESSION AUTHORIZATION; DROP SCHEMA tststats CASCADE; +DROP SCHEMA sts_sch1 CASCADE; DROP USER regress_stats_user1;