Skip to content

Commit 2393d37

Browse files
Check for CREATE privilege on the schema in CREATE STATISTICS.
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 <postgres@jeltef.nl> Author: Jelte Fennema-Nio <postgres@jeltef.nl> Co-authored-by: Nathan Bossart <nathandbossart@gmail.com> Reviewed-by: Noah Misch <noah@leadboat.com> Reviewed-by: Álvaro Herrera <alvherre@kurilemu.de> Security: CVE-2025-12817 Backpatch-through: 13
1 parent 9142156 commit 2393d37

File tree

6 files changed

+56
-4
lines changed

6 files changed

+56
-4
lines changed

src/backend/commands/statscmds.c

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ compare_int16(const void *a, const void *b)
6262
* CREATE STATISTICS
6363
*/
6464
ObjectAddress
65-
CreateStatistics(CreateStatsStmt *stmt)
65+
CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
6666
{
6767
int16 attnums[STATS_MAX_DIMENSIONS];
6868
int nattnums = 0;
@@ -173,6 +173,20 @@ CreateStatistics(CreateStatsStmt *stmt)
173173
}
174174
namestrcpy(&stxname, namestr);
175175

176+
/*
177+
* Check we have creation rights in target namespace. Skip check if
178+
* caller doesn't want it.
179+
*/
180+
if (check_rights)
181+
{
182+
AclResult aclresult;
183+
184+
aclresult = pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE);
185+
if (aclresult != ACLCHECK_OK)
186+
aclcheck_error(aclresult, OBJECT_SCHEMA,
187+
get_namespace_name(namespaceId));
188+
}
189+
176190
/*
177191
* Deal with the possibility that the statistics object already exists.
178192
*/

src/backend/commands/tablecmds.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8832,7 +8832,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
88328832
/* The CreateStatsStmt has already been through transformStatsStmt */
88338833
Assert(stmt->transformed);
88348834

8835-
address = CreateStatistics(stmt);
8835+
address = CreateStatistics(stmt, !is_rebuild);
88368836

88378837
return address;
88388838
}

src/backend/tcop/utility.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1891,7 +1891,7 @@ ProcessUtilitySlow(ParseState *pstate,
18911891
/* Run parse analysis ... */
18921892
stmt = transformStatsStmt(relid, stmt, queryString);
18931893

1894-
address = CreateStatistics(stmt);
1894+
address = CreateStatistics(stmt, true);
18951895
}
18961896
break;
18971897

src/include/commands/defrem.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ extern void RemoveOperatorById(Oid operOid);
8080
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
8181

8282
/* commands/statscmds.c */
83-
extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt);
83+
extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights);
8484
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
8585
extern void RemoveStatisticsById(Oid statsOid);
8686
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);

src/test/regress/expected/stats_ext.out

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3392,6 +3392,23 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
33923392
s_expr | {1}
33933393
(2 rows)
33943394

3395+
-- CREATE STATISTICS checks for CREATE on the schema
3396+
RESET SESSION AUTHORIZATION;
3397+
CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT);
3398+
GRANT USAGE ON SCHEMA sts_sch1 TO regress_stats_user1;
3399+
ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1;
3400+
SET SESSION AUTHORIZATION regress_stats_user1;
3401+
CREATE STATISTICS sts_sch1.fail ON a, b FROM sts_sch1.tbl;
3402+
ERROR: permission denied for schema sts_sch1
3403+
RESET SESSION AUTHORIZATION;
3404+
GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1;
3405+
SET SESSION AUTHORIZATION regress_stats_user1;
3406+
CREATE STATISTICS sts_sch1.pass ON a, b FROM sts_sch1.tbl;
3407+
-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema
3408+
RESET SESSION AUTHORIZATION;
3409+
REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1;
3410+
SET SESSION AUTHORIZATION regress_stats_user1;
3411+
ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT;
33953412
-- Tidy up
33963413
DROP OPERATOR <<< (int, int);
33973414
DROP FUNCTION op_leak(int, int);
@@ -3404,4 +3421,6 @@ NOTICE: drop cascades to 3 other objects
34043421
DETAIL: drop cascades to table tststats.priv_test_parent_tbl
34053422
drop cascades to table tststats.priv_test_tbl
34063423
drop cascades to view tststats.priv_test_view
3424+
DROP SCHEMA sts_sch1 CASCADE;
3425+
NOTICE: drop cascades to table sts_sch1.tbl
34073426
DROP USER regress_stats_user1;

src/test/regress/sql/stats_ext.sql

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,24 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x
17311731
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
17321732
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
17331733

1734+
-- CREATE STATISTICS checks for CREATE on the schema
1735+
RESET SESSION AUTHORIZATION;
1736+
CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT);
1737+
GRANT USAGE ON SCHEMA sts_sch1 TO regress_stats_user1;
1738+
ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1;
1739+
SET SESSION AUTHORIZATION regress_stats_user1;
1740+
CREATE STATISTICS sts_sch1.fail ON a, b FROM sts_sch1.tbl;
1741+
RESET SESSION AUTHORIZATION;
1742+
GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1;
1743+
SET SESSION AUTHORIZATION regress_stats_user1;
1744+
CREATE STATISTICS sts_sch1.pass ON a, b FROM sts_sch1.tbl;
1745+
1746+
-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema
1747+
RESET SESSION AUTHORIZATION;
1748+
REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1;
1749+
SET SESSION AUTHORIZATION regress_stats_user1;
1750+
ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT;
1751+
17341752
-- Tidy up
17351753
DROP OPERATOR <<< (int, int);
17361754
DROP FUNCTION op_leak(int, int);
@@ -1739,4 +1757,5 @@ DROP FUNCTION op_leak(record, record);
17391757
RESET SESSION AUTHORIZATION;
17401758
DROP TABLE stats_ext_tbl;
17411759
DROP SCHEMA tststats CASCADE;
1760+
DROP SCHEMA sts_sch1 CASCADE;
17421761
DROP USER regress_stats_user1;

0 commit comments

Comments
 (0)