1
0
mirror of https://github.com/postgres/postgres.git synced 2025-11-13 16:22:44 +03:00

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
This commit is contained in:
Nathan Bossart
2025-11-10 09:00:00 -06:00
parent 600086f471
commit 5e4fcbe531
6 changed files with 90 additions and 4 deletions

View File

@@ -60,7 +60,7 @@ compare_int16(const void *a, const void *b)
* CREATE STATISTICS * CREATE STATISTICS
*/ */
ObjectAddress ObjectAddress
CreateStatistics(CreateStatsStmt *stmt) CreateStatistics(CreateStatsStmt *stmt, bool check_rights)
{ {
int16 attnums[STATS_MAX_DIMENSIONS]; int16 attnums[STATS_MAX_DIMENSIONS];
int nattnums = 0; int nattnums = 0;
@@ -170,6 +170,21 @@ CreateStatistics(CreateStatsStmt *stmt)
} }
namestrcpy(&stxname, namestr); 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. * Deal with the possibility that the statistics object already exists.
*/ */

View File

@@ -9682,7 +9682,7 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
/* The CreateStatsStmt has already been through transformStatsStmt */ /* The CreateStatsStmt has already been through transformStatsStmt */
Assert(stmt->transformed); Assert(stmt->transformed);
address = CreateStatistics(stmt); address = CreateStatistics(stmt, !is_rebuild);
return address; return address;
} }

View File

@@ -1900,7 +1900,7 @@ ProcessUtilitySlow(ParseState *pstate,
/* Run parse analysis ... */ /* Run parse analysis ... */
stmt = transformStatsStmt(relid, stmt, queryString); stmt = transformStatsStmt(relid, stmt, queryString);
address = CreateStatistics(stmt); address = CreateStatistics(stmt, true);
} }
break; break;

View File

@@ -85,7 +85,7 @@ extern void RemoveOperatorById(Oid operOid);
extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt); extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
/* commands/statscmds.c */ /* commands/statscmds.c */
extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt); extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights);
extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt); extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
extern void RemoveStatisticsById(Oid statsOid); extern void RemoveStatisticsById(Oid statsOid);
extern void RemoveStatisticsDataById(Oid statsOid, bool inh); extern void RemoveStatisticsDataById(Oid statsOid, bool inh);

View File

@@ -3451,6 +3451,41 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
s_expr | {1} s_expr | {1}
(2 rows) (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, c INT GENERATED ALWAYS AS (b * 2) STORED);
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, c FROM sts_sch1.tbl;
ERROR: permission denied for schema sts_sch1
CREATE STATISTICS sts_sch2.fail ON a, b, c 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, c FROM sts_sch1.tbl;
CREATE STATISTICS sts_sch2.fail ON a, b, c 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, c FROM sts_sch1.tbl;
ERROR: permission denied for schema sts_sch1
CREATE STATISTICS sts_sch2.pass1 ON a, b, c 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, c FROM sts_sch1.tbl;
CREATE STATISTICS sts_sch2.pass2 ON a, b, c 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;
ALTER TABLE sts_sch1.tbl ALTER COLUMN c SET EXPRESSION AS (a * 3);
-- Tidy up -- Tidy up
DROP OPERATOR <<< (int, int); DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int); DROP FUNCTION op_leak(int, int);
@@ -3463,6 +3498,8 @@ NOTICE: drop cascades to 3 other objects
DETAIL: drop cascades to table tststats.priv_test_parent_tbl DETAIL: drop cascades to table tststats.priv_test_parent_tbl
drop cascades to table tststats.priv_test_tbl drop cascades to table tststats.priv_test_tbl
drop cascades to view tststats.priv_test_view 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; DROP USER regress_stats_user1;
CREATE TABLE grouping_unique (x integer); CREATE TABLE grouping_unique (x integer);
INSERT INTO grouping_unique (x) SELECT gs FROM generate_series(1,1000) AS gs; INSERT INTO grouping_unique (x) SELECT gs FROM generate_series(1,1000) AS gs;

View File

@@ -1759,6 +1759,39 @@ SELECT statistics_name, most_common_vals FROM pg_stats_ext x
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(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, c INT GENERATED ALWAYS AS (b * 2) STORED);
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, c FROM sts_sch1.tbl;
CREATE STATISTICS sts_sch2.fail ON a, b, c 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, c FROM sts_sch1.tbl;
CREATE STATISTICS sts_sch2.fail ON a, b, c 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, c FROM sts_sch1.tbl;
CREATE STATISTICS sts_sch2.pass1 ON a, b, c 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, c FROM sts_sch1.tbl;
CREATE STATISTICS sts_sch2.pass2 ON a, b, c 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;
ALTER TABLE sts_sch1.tbl ALTER COLUMN c SET EXPRESSION AS (a * 3);
-- Tidy up -- Tidy up
DROP OPERATOR <<< (int, int); DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int); DROP FUNCTION op_leak(int, int);
@@ -1767,6 +1800,7 @@ DROP FUNCTION op_leak(record, record);
RESET SESSION AUTHORIZATION; RESET SESSION AUTHORIZATION;
DROP TABLE stats_ext_tbl; DROP TABLE stats_ext_tbl;
DROP SCHEMA tststats CASCADE; DROP SCHEMA tststats CASCADE;
DROP SCHEMA sts_sch1, sts_sch2 CASCADE;
DROP USER regress_stats_user1; DROP USER regress_stats_user1;
CREATE TABLE grouping_unique (x integer); CREATE TABLE grouping_unique (x integer);