From 95cce56696867c7629d12c3ba1da8edd5bedaa8e 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 | 16 +++++++++++++++- src/backend/commands/tablecmds.c | 2 +- src/backend/tcop/utility.c | 2 +- src/include/commands/defrem.h | 2 +- src/test/regress/expected/stats_ext.out | 19 +++++++++++++++++++ src/test/regress/sql/stats_ext.sql | 19 +++++++++++++++++++ 6 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c index df6cb4a6f49..b06758f5e85 100644 --- a/src/backend/commands/statscmds.c +++ b/src/backend/commands/statscmds.c @@ -62,7 +62,7 @@ compare_int16(const void *a, const void *b) * CREATE STATISTICS */ ObjectAddress -CreateStatistics(CreateStatsStmt *stmt) +CreateStatistics(CreateStatsStmt *stmt, bool check_rights) { int16 attnums[STATS_MAX_DIMENSIONS]; int nattnums = 0; @@ -175,6 +175,20 @@ CreateStatistics(CreateStatsStmt *stmt) } namestrcpy(&stxname, namestr); + /* + * Check we have creation rights in target namespace. Skip check if + * caller doesn't want it. + */ + if (check_rights) + { + AclResult aclresult; + + 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/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 84f2ace5cb8..9c70c024205 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -8662,7 +8662,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel, /* The CreateStatsStmt has already been through transformStatsStmt */ Assert(stmt->transformed); - address = CreateStatistics(stmt); + address = CreateStatistics(stmt, !is_rebuild); return address; } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 2b1cb364896..c43950787ae 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -1881,7 +1881,7 @@ ProcessUtilitySlow(ParseState *pstate, /* Run parse analysis ... */ stmt = transformStatsStmt(relid, stmt, queryString); - address = CreateStatistics(stmt); + address = CreateStatistics(stmt, true); } break; diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h index 42bf1c75198..1e6339dda1a 100644 --- a/src/include/commands/defrem.h +++ b/src/include/commands/defrem.h @@ -80,7 +80,7 @@ extern void RemoveOperatorById(Oid operOid); extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt); /* commands/statscmds.c */ -extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt); +extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights); extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt); extern void RemoveStatisticsById(Oid statsOid); extern Oid StatisticsGetRelation(Oid statId, bool missing_ok); diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out index 84b940dd8e5..e18f3953d06 100644 --- a/src/test/regress/expected/stats_ext.out +++ b/src/test/regress/expected/stats_ext.out @@ -3371,6 +3371,23 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x s_expr | {1} (2 rows) +-- 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; +-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT; -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); @@ -3383,4 +3400,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 b631cd2d181..c6623df1ffb 100644 --- a/src/test/regress/sql/stats_ext.sql +++ b/src/test/regress/sql/stats_ext.sql @@ -1729,6 +1729,24 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*); +-- 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; + +-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema +RESET SESSION AUTHORIZATION; +REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1; +SET SESSION AUTHORIZATION regress_stats_user1; +ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT; + -- Tidy up DROP OPERATOR <<< (int, int); DROP FUNCTION op_leak(int, int); @@ -1737,4 +1755,5 @@ DROP FUNCTION op_leak(record, record); RESET SESSION AUTHORIZATION; DROP TABLE stats_ext_tbl; DROP SCHEMA tststats CASCADE; +DROP SCHEMA sts_sch1 CASCADE; DROP USER regress_stats_user1;