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/test/regress/expected/stats_ext.out
src/test/regress/sql/stats_ext.sql

index f3a6c2e7cc01e621089d63775a1676ad3a139a08..b524a08ec7e52f60bf5efcbbb76bdce48260b450 100644 (file)
@@ -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.
     */
index fa06298504e1b6f819ed1b5b165223db55f29aa2..b5b3e5dc81c8ac4e79c21357ece0891f8922491d 100644 (file)
@@ -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;
index dd632f894e52be549041d39b30bf60c7093c2f48..72e6489bbb7a75dbee9bf65615ef530be90c6126 100644 (file)
@@ -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;