Check for CREATE privilege on the schema in CREATE STATISTICS.
authorNathan Bossart <[email protected]>
Mon, 10 Nov 2025 15:00:00 +0000 (09:00 -0600)
committerNathan Bossart <[email protected]>
Mon, 10 Nov 2025 15:00:00 +0000 (09:00 -0600)
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 <[email protected]>
Author: Jelte Fennema-Nio <[email protected]>
Co-authored-by: Nathan Bossart <[email protected]>
Reviewed-by: Noah Misch <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Security: CVE-2025-12817
Backpatch-through: 13

src/backend/commands/statscmds.c
src/backend/commands/tablecmds.c
src/backend/tcop/utility.c
src/include/commands/defrem.h
src/test/regress/expected/stats_ext.out
src/test/regress/sql/stats_ext.sql

index 3568edbb553bacdf5ca1a6cfc11e1bb95e112163..f2546c9bf0887910e9656d6791b6875d49c1606e 100644 (file)
@@ -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;
@@ -172,6 +172,21 @@ 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 = object_aclcheck(NamespaceRelationId, 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.
     */
index a5d1b458c6942c358aeffe2f9242d8e95227bc70..7aae9d208c449691a6908a63fbb00a3bf330f028 100644 (file)
@@ -8873,7 +8873,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;
 }
index 7e0114f6218c4d3a968988638528d2831bb3b233..b366394ca8116c71f5571cb03dc13d1825abbbcc 100644 (file)
@@ -1902,7 +1902,7 @@ ProcessUtilitySlow(ParseState *pstate,
                    /* Run parse analysis ... */
                    stmt = transformStatsStmt(relid, stmt, queryString);
 
-                   address = CreateStatistics(stmt);
+                   address = CreateStatistics(stmt, true);
                }
                break;
 
index 478203ed4c4d35aff73fd7d444da04d3fb75e64d..83423d3ba13345a125e620e8d850e2edd052d7a5 100644 (file)
@@ -81,7 +81,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 void RemoveStatisticsDataById(Oid statsOid, bool inh);
index 6bd4739e08ecc9140baf5c6eeeb725f62d47615f..d3fd25326c824b1460f93a5e17228e5ca6f0f7dd 100644 (file)
@@ -3407,6 +3407,40 @@ 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);
+CREATE SCHEMA sts_sch2;
+GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
+ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1;
+SET SESSION AUTHORIZATION regress_stats_user1;
+CREATE STATISTICS ON a, b FROM sts_sch1.tbl;
+ERROR:  permission denied for schema sts_sch1
+CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl;
+ERROR:  permission denied for schema sts_sch2
+RESET SESSION AUTHORIZATION;
+GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1;
+SET SESSION AUTHORIZATION regress_stats_user1;
+CREATE STATISTICS ON a, b FROM sts_sch1.tbl;
+CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl;
+ERROR:  permission denied for schema sts_sch2
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1;
+GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1;
+SET SESSION AUTHORIZATION regress_stats_user1;
+CREATE STATISTICS ON a, b FROM sts_sch1.tbl;
+ERROR:  permission denied for schema sts_sch1
+CREATE STATISTICS sts_sch2.pass1 ON a, b FROM sts_sch1.tbl;
+RESET SESSION AUTHORIZATION;
+GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
+SET SESSION AUTHORIZATION regress_stats_user1;
+CREATE STATISTICS ON a, b FROM sts_sch1.tbl;
+CREATE STATISTICS sts_sch2.pass2 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, sts_sch2 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);
@@ -3419,4 +3453,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, sts_sch2 CASCADE;
+NOTICE:  drop cascades to table sts_sch1.tbl
 DROP USER regress_stats_user1;
index a83ea3d5679aaacbf983a9c92caa8c7147867e82..39433085a21610dc6f61d32662d2efba839e165e 100644 (file)
@@ -1739,6 +1739,38 @@ 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);
+CREATE SCHEMA sts_sch2;
+GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
+ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1;
+SET SESSION AUTHORIZATION regress_stats_user1;
+CREATE STATISTICS ON a, b FROM sts_sch1.tbl;
+CREATE STATISTICS sts_sch2.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 ON a, b FROM sts_sch1.tbl;
+CREATE STATISTICS sts_sch2.fail ON a, b FROM sts_sch1.tbl;
+RESET SESSION AUTHORIZATION;
+REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1;
+GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1;
+SET SESSION AUTHORIZATION regress_stats_user1;
+CREATE STATISTICS ON a, b FROM sts_sch1.tbl;
+CREATE STATISTICS sts_sch2.pass1 ON a, b FROM sts_sch1.tbl;
+RESET SESSION AUTHORIZATION;
+GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
+SET SESSION AUTHORIZATION regress_stats_user1;
+CREATE STATISTICS ON a, b FROM sts_sch1.tbl;
+CREATE STATISTICS sts_sch2.pass2 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, sts_sch2 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);
@@ -1747,4 +1779,5 @@ DROP FUNCTION op_leak(record, record);
 RESET SESSION AUTHORIZATION;
 DROP TABLE stats_ext_tbl;
 DROP SCHEMA tststats CASCADE;
+DROP SCHEMA sts_sch1, sts_sch2 CASCADE;
 DROP USER regress_stats_user1;