Skip to content

Commit 00eb646

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 7eb8fca commit 00eb646

File tree

6 files changed

+90
-4
lines changed

6 files changed

+90
-4
lines changed

src/backend/commands/statscmds.c

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ compare_int16(const void *a, const void *b)
5959
* CREATE STATISTICS
6060
*/
6161
ObjectAddress
62-
CreateStatistics(CreateStatsStmt *stmt)
62+
CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
6363
{
6464
int16 attnums[STATS_MAX_DIMENSIONS];
6565
int nattnums = 0;
@@ -169,6 +169,21 @@ CreateStatistics(CreateStatsStmt *stmt)
169169
}
170170
namestrcpy(&stxname, namestr);
171171

172+
/*
173+
* Check we have creation rights in target namespace. Skip check if
174+
* caller doesn't want it.
175+
*/
176+
if (check_rights)
177+
{
178+
AclResult aclresult;
179+
180+
aclresult = object_aclcheck(NamespaceRelationId, namespaceId,
181+
GetUserId(), ACL_CREATE);
182+
if (aclresult != ACLCHECK_OK)
183+
aclcheck_error(aclresult, OBJECT_SCHEMA,
184+
get_namespace_name(namespaceId));
185+
}
186+
172187
/*
173188
* Deal with the possibility that the statistics object already exists.
174189
*/

src/backend/commands/tablecmds.c

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

9683-
address = CreateStatistics(stmt);
9683+
address = CreateStatistics(stmt, !is_rebuild);
96849684

96859685
return address;
96869686
}

src/backend/tcop/utility.c

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

1904-
address = CreateStatistics(stmt);
1904+
address = CreateStatistics(stmt, true);
19051905
}
19061906
break;
19071907

src/include/commands/defrem.h

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

8787
/* commands/statscmds.c */
88-
extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt);
88+
extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights);
8989
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
9090
extern void RemoveStatisticsById(Oid statsOid);
9191
extern void RemoveStatisticsDataById(Oid statsOid, bool inh);

src/test/regress/expected/stats_ext.out

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3451,6 +3451,41 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
34513451
s_expr | {1}
34523452
(2 rows)
34533453

3454+
-- CREATE STATISTICS checks for CREATE on the schema
3455+
RESET SESSION AUTHORIZATION;
3456+
CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT, c INT GENERATED ALWAYS AS (b * 2) STORED);
3457+
CREATE SCHEMA sts_sch2;
3458+
GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
3459+
ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1;
3460+
SET SESSION AUTHORIZATION regress_stats_user1;
3461+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
3462+
ERROR: permission denied for schema sts_sch1
3463+
CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl;
3464+
ERROR: permission denied for schema sts_sch2
3465+
RESET SESSION AUTHORIZATION;
3466+
GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1;
3467+
SET SESSION AUTHORIZATION regress_stats_user1;
3468+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
3469+
CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl;
3470+
ERROR: permission denied for schema sts_sch2
3471+
RESET SESSION AUTHORIZATION;
3472+
REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1;
3473+
GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1;
3474+
SET SESSION AUTHORIZATION regress_stats_user1;
3475+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
3476+
ERROR: permission denied for schema sts_sch1
3477+
CREATE STATISTICS sts_sch2.pass1 ON a, b, c FROM sts_sch1.tbl;
3478+
RESET SESSION AUTHORIZATION;
3479+
GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
3480+
SET SESSION AUTHORIZATION regress_stats_user1;
3481+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
3482+
CREATE STATISTICS sts_sch2.pass2 ON a, b, c FROM sts_sch1.tbl;
3483+
-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema
3484+
RESET SESSION AUTHORIZATION;
3485+
REVOKE CREATE ON SCHEMA sts_sch1, sts_sch2 FROM regress_stats_user1;
3486+
SET SESSION AUTHORIZATION regress_stats_user1;
3487+
ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT;
3488+
ALTER TABLE sts_sch1.tbl ALTER COLUMN c SET EXPRESSION AS (a * 3);
34543489
-- Tidy up
34553490
DROP OPERATOR <<< (int, int);
34563491
DROP FUNCTION op_leak(int, int);
@@ -3463,6 +3498,8 @@ NOTICE: drop cascades to 3 other objects
34633498
DETAIL: drop cascades to table tststats.priv_test_parent_tbl
34643499
drop cascades to table tststats.priv_test_tbl
34653500
drop cascades to view tststats.priv_test_view
3501+
DROP SCHEMA sts_sch1, sts_sch2 CASCADE;
3502+
NOTICE: drop cascades to table sts_sch1.tbl
34663503
DROP USER regress_stats_user1;
34673504
CREATE TABLE grouping_unique (x integer);
34683505
INSERT INTO grouping_unique (x) SELECT gs FROM generate_series(1,1000) AS gs;

src/test/regress/sql/stats_ext.sql

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1759,6 +1759,39 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x
17591759
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
17601760
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
17611761

1762+
-- CREATE STATISTICS checks for CREATE on the schema
1763+
RESET SESSION AUTHORIZATION;
1764+
CREATE SCHEMA sts_sch1 CREATE TABLE sts_sch1.tbl (a INT, b INT, c INT GENERATED ALWAYS AS (b * 2) STORED);
1765+
CREATE SCHEMA sts_sch2;
1766+
GRANT USAGE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
1767+
ALTER TABLE sts_sch1.tbl OWNER TO regress_stats_user1;
1768+
SET SESSION AUTHORIZATION regress_stats_user1;
1769+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
1770+
CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl;
1771+
RESET SESSION AUTHORIZATION;
1772+
GRANT CREATE ON SCHEMA sts_sch1 TO regress_stats_user1;
1773+
SET SESSION AUTHORIZATION regress_stats_user1;
1774+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
1775+
CREATE STATISTICS sts_sch2.fail ON a, b, c FROM sts_sch1.tbl;
1776+
RESET SESSION AUTHORIZATION;
1777+
REVOKE CREATE ON SCHEMA sts_sch1 FROM regress_stats_user1;
1778+
GRANT CREATE ON SCHEMA sts_sch2 TO regress_stats_user1;
1779+
SET SESSION AUTHORIZATION regress_stats_user1;
1780+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
1781+
CREATE STATISTICS sts_sch2.pass1 ON a, b, c FROM sts_sch1.tbl;
1782+
RESET SESSION AUTHORIZATION;
1783+
GRANT CREATE ON SCHEMA sts_sch1, sts_sch2 TO regress_stats_user1;
1784+
SET SESSION AUTHORIZATION regress_stats_user1;
1785+
CREATE STATISTICS ON a, b, c FROM sts_sch1.tbl;
1786+
CREATE STATISTICS sts_sch2.pass2 ON a, b, c FROM sts_sch1.tbl;
1787+
1788+
-- re-creating statistics via ALTER TABLE bypasses checks for CREATE on schema
1789+
RESET SESSION AUTHORIZATION;
1790+
REVOKE CREATE ON SCHEMA sts_sch1, sts_sch2 FROM regress_stats_user1;
1791+
SET SESSION AUTHORIZATION regress_stats_user1;
1792+
ALTER TABLE sts_sch1.tbl ALTER COLUMN a TYPE SMALLINT;
1793+
ALTER TABLE sts_sch1.tbl ALTER COLUMN c SET EXPRESSION AS (a * 3);
1794+
17621795
-- Tidy up
17631796
DROP OPERATOR <<< (int, int);
17641797
DROP FUNCTION op_leak(int, int);
@@ -1767,6 +1800,7 @@ DROP FUNCTION op_leak(record, record);
17671800
RESET SESSION AUTHORIZATION;
17681801
DROP TABLE stats_ext_tbl;
17691802
DROP SCHEMA tststats CASCADE;
1803+
DROP SCHEMA sts_sch1, sts_sch2 CASCADE;
17701804
DROP USER regress_stats_user1;
17711805

17721806
CREATE TABLE grouping_unique (x integer);

0 commit comments

Comments
 (0)