Skip to content

Commit 9cc2b62

Browse files
Fix privilege checks in pg_stats_ext and pg_stats_ext_exprs.
The catalog view pg_stats_ext fails to consider privileges for expression statistics. The catalog view pg_stats_ext_exprs fails to consider privileges and row-level security policies. To fix, restrict the data in these views to table owners or roles that inherit privileges of the table owner. It may be possible to apply less restrictive privilege checks in some cases, but that is left as a future exercise. Furthermore, for pg_stats_ext_exprs, do not return data for tables with row-level security enabled, as is already done for pg_stats_ext. On the back-branches, a fix-CVE-2024-4317.sql script is provided that will install into the "share" directory. This file can be used to apply the fix to existing clusters. Bumps catversion on 'master' branch only. Reported-by: Lukas Fittl Reviewed-by: Noah Misch, Tomas Vondra, Tom Lane Security: CVE-2024-4317 Backpatch-through: 14
1 parent 3672c6c commit 9cc2b62

File tree

8 files changed

+199
-17
lines changed

8 files changed

+199
-17
lines changed

doc/src/sgml/catalogs.sgml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7685,8 +7685,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
76857685
is a publicly readable view
76867686
on <structname>pg_statistic_ext_data</structname> (after joining
76877687
with <link linkend="catalog-pg-statistic-ext"><structname>pg_statistic_ext</structname></link>) that only exposes
7688-
information about those tables and columns that are readable by the
7689-
current user.
7688+
information about tables the current user owns.
76907689
</para>
76917690

76927691
<table>

doc/src/sgml/system-views.sgml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3741,7 +3741,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
37413741
and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
37423742
catalogs. This view allows access only to rows of
37433743
<link linkend="catalog-pg-statistic-ext"><structname>pg_statistic_ext</structname></link> and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
3744-
that correspond to tables the user has permission to read, and therefore
3744+
that correspond to tables the user owns, and therefore
37453745
it is safe to allow public read access to this view.
37463746
</para>
37473747

@@ -3952,7 +3952,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
39523952
and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
39533953
catalogs. This view allows access only to rows of
39543954
<link linkend="catalog-pg-statistic-ext"><structname>pg_statistic_ext</structname></link> and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
3955-
that correspond to tables the user has permission to read, and therefore
3955+
that correspond to tables the user owns, and therefore
39563956
it is safe to allow public read access to this view.
39573957
</para>
39583958

src/backend/catalog/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,14 @@ install-data: bki-stamp installdirs
130130
$(INSTALL_DATA) $(srcdir)/system_views.sql '$(DESTDIR)$(datadir)/system_views.sql'
131131
$(INSTALL_DATA) $(srcdir)/information_schema.sql '$(DESTDIR)$(datadir)/information_schema.sql'
132132
$(INSTALL_DATA) $(srcdir)/sql_features.txt '$(DESTDIR)$(datadir)/sql_features.txt'
133+
$(INSTALL_DATA) $(srcdir)/fix-CVE-2024-4317.sql '$(DESTDIR)$(datadir)/fix-CVE-2024-4317.sql'
133134

134135
installdirs:
135136
$(MKDIR_P) '$(DESTDIR)$(datadir)'
136137

137138
.PHONY: uninstall-data
138139
uninstall-data:
139-
rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql system_functions.sql system_views.sql information_schema.sql sql_features.txt)
140+
rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql system_functions.sql system_views.sql information_schema.sql sql_features.txt fix-CVE-2024-4317.sql)
140141

141142
# postgres.bki, system_constraints.sql, and the generated headers are
142143
# in the distribution tarball, so they are not cleaned here.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* fix-CVE-2024-4317.sql
3+
*
4+
* Copyright (c) 2024, PostgreSQL Global Development Group
5+
*
6+
* src/backend/catalog/fix-CVE-2024-4317.sql
7+
*
8+
* This file should be run in every database in the cluster to address
9+
* CVE-2024-4317.
10+
*/
11+
12+
SET search_path = pg_catalog;
13+
14+
CREATE OR REPLACE VIEW pg_stats_ext WITH (security_barrier) AS
15+
SELECT cn.nspname AS schemaname,
16+
c.relname AS tablename,
17+
sn.nspname AS statistics_schemaname,
18+
s.stxname AS statistics_name,
19+
pg_get_userbyid(s.stxowner) AS statistics_owner,
20+
( SELECT array_agg(a.attname ORDER BY a.attnum)
21+
FROM unnest(s.stxkeys) k
22+
JOIN pg_attribute a
23+
ON (a.attrelid = s.stxrelid AND a.attnum = k)
24+
) AS attnames,
25+
pg_get_statisticsobjdef_expressions(s.oid) as exprs,
26+
s.stxkind AS kinds,
27+
sd.stxdinherit AS inherited,
28+
sd.stxdndistinct AS n_distinct,
29+
sd.stxddependencies AS dependencies,
30+
m.most_common_vals,
31+
m.most_common_val_nulls,
32+
m.most_common_freqs,
33+
m.most_common_base_freqs
34+
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
35+
JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
36+
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
37+
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
38+
LEFT JOIN LATERAL
39+
( SELECT array_agg(values) AS most_common_vals,
40+
array_agg(nulls) AS most_common_val_nulls,
41+
array_agg(frequency) AS most_common_freqs,
42+
array_agg(base_frequency) AS most_common_base_freqs
43+
FROM pg_mcv_list_items(sd.stxdmcv)
44+
) m ON sd.stxdmcv IS NOT NULL
45+
WHERE pg_has_role(c.relowner, 'USAGE')
46+
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
47+
48+
CREATE OR REPLACE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
49+
SELECT cn.nspname AS schemaname,
50+
c.relname AS tablename,
51+
sn.nspname AS statistics_schemaname,
52+
s.stxname AS statistics_name,
53+
pg_get_userbyid(s.stxowner) AS statistics_owner,
54+
stat.expr,
55+
sd.stxdinherit AS inherited,
56+
(stat.a).stanullfrac AS null_frac,
57+
(stat.a).stawidth AS avg_width,
58+
(stat.a).stadistinct AS n_distinct,
59+
(CASE
60+
WHEN (stat.a).stakind1 = 1 THEN (stat.a).stavalues1
61+
WHEN (stat.a).stakind2 = 1 THEN (stat.a).stavalues2
62+
WHEN (stat.a).stakind3 = 1 THEN (stat.a).stavalues3
63+
WHEN (stat.a).stakind4 = 1 THEN (stat.a).stavalues4
64+
WHEN (stat.a).stakind5 = 1 THEN (stat.a).stavalues5
65+
END) AS most_common_vals,
66+
(CASE
67+
WHEN (stat.a).stakind1 = 1 THEN (stat.a).stanumbers1
68+
WHEN (stat.a).stakind2 = 1 THEN (stat.a).stanumbers2
69+
WHEN (stat.a).stakind3 = 1 THEN (stat.a).stanumbers3
70+
WHEN (stat.a).stakind4 = 1 THEN (stat.a).stanumbers4
71+
WHEN (stat.a).stakind5 = 1 THEN (stat.a).stanumbers5
72+
END) AS most_common_freqs,
73+
(CASE
74+
WHEN (stat.a).stakind1 = 2 THEN (stat.a).stavalues1
75+
WHEN (stat.a).stakind2 = 2 THEN (stat.a).stavalues2
76+
WHEN (stat.a).stakind3 = 2 THEN (stat.a).stavalues3
77+
WHEN (stat.a).stakind4 = 2 THEN (stat.a).stavalues4
78+
WHEN (stat.a).stakind5 = 2 THEN (stat.a).stavalues5
79+
END) AS histogram_bounds,
80+
(CASE
81+
WHEN (stat.a).stakind1 = 3 THEN (stat.a).stanumbers1[1]
82+
WHEN (stat.a).stakind2 = 3 THEN (stat.a).stanumbers2[1]
83+
WHEN (stat.a).stakind3 = 3 THEN (stat.a).stanumbers3[1]
84+
WHEN (stat.a).stakind4 = 3 THEN (stat.a).stanumbers4[1]
85+
WHEN (stat.a).stakind5 = 3 THEN (stat.a).stanumbers5[1]
86+
END) correlation,
87+
(CASE
88+
WHEN (stat.a).stakind1 = 4 THEN (stat.a).stavalues1
89+
WHEN (stat.a).stakind2 = 4 THEN (stat.a).stavalues2
90+
WHEN (stat.a).stakind3 = 4 THEN (stat.a).stavalues3
91+
WHEN (stat.a).stakind4 = 4 THEN (stat.a).stavalues4
92+
WHEN (stat.a).stakind5 = 4 THEN (stat.a).stavalues5
93+
END) AS most_common_elems,
94+
(CASE
95+
WHEN (stat.a).stakind1 = 4 THEN (stat.a).stanumbers1
96+
WHEN (stat.a).stakind2 = 4 THEN (stat.a).stanumbers2
97+
WHEN (stat.a).stakind3 = 4 THEN (stat.a).stanumbers3
98+
WHEN (stat.a).stakind4 = 4 THEN (stat.a).stanumbers4
99+
WHEN (stat.a).stakind5 = 4 THEN (stat.a).stanumbers5
100+
END) AS most_common_elem_freqs,
101+
(CASE
102+
WHEN (stat.a).stakind1 = 5 THEN (stat.a).stanumbers1
103+
WHEN (stat.a).stakind2 = 5 THEN (stat.a).stanumbers2
104+
WHEN (stat.a).stakind3 = 5 THEN (stat.a).stanumbers3
105+
WHEN (stat.a).stakind4 = 5 THEN (stat.a).stanumbers4
106+
WHEN (stat.a).stakind5 = 5 THEN (stat.a).stanumbers5
107+
END) AS elem_count_histogram
108+
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
109+
LEFT JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
110+
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
111+
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
112+
JOIN LATERAL (
113+
SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
114+
unnest(sd.stxdexpr)::pg_statistic AS a
115+
) stat ON (stat.expr IS NOT NULL)
116+
WHERE pg_has_role(c.relowner, 'USAGE')
117+
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));

src/backend/catalog/system_views.sql

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,7 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS
284284
array_agg(base_frequency) AS most_common_base_freqs
285285
FROM pg_mcv_list_items(sd.stxdmcv)
286286
) m ON sd.stxdmcv IS NOT NULL
287-
WHERE NOT EXISTS
288-
( SELECT 1
289-
FROM unnest(stxkeys) k
290-
JOIN pg_attribute a
291-
ON (a.attrelid = s.stxrelid AND a.attnum = k)
292-
WHERE NOT has_column_privilege(c.oid, a.attnum, 'select') )
287+
WHERE pg_has_role(c.relowner, 'USAGE')
293288
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
294289

295290
CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
@@ -359,7 +354,9 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
359354
JOIN LATERAL (
360355
SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
361356
unnest(sd.stxdexpr)::pg_statistic AS a
362-
) stat ON (stat.expr IS NOT NULL);
357+
) stat ON (stat.expr IS NOT NULL)
358+
WHERE pg_has_role(c.relowner, 'USAGE')
359+
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
363360

364361
-- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data
365362
REVOKE ALL ON pg_statistic_ext_data FROM public;

src/test/regress/expected/rules.out

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2453,10 +2453,7 @@ pg_stats_ext| SELECT cn.nspname AS schemaname,
24532453
array_agg(pg_mcv_list_items.frequency) AS most_common_freqs,
24542454
array_agg(pg_mcv_list_items.base_frequency) AS most_common_base_freqs
24552455
FROM pg_mcv_list_items(sd.stxdmcv) pg_mcv_list_items(index, "values", nulls, frequency, base_frequency)) m ON ((sd.stxdmcv IS NOT NULL)))
2456-
WHERE ((NOT (EXISTS ( SELECT 1
2457-
FROM (unnest(s.stxkeys) k(k)
2458-
JOIN pg_attribute a ON (((a.attrelid = s.stxrelid) AND (a.attnum = k.k))))
2459-
WHERE (NOT has_column_privilege(c.oid, a.attnum, 'select'::text))))) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
2456+
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
24602457
pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
24612458
c.relname AS tablename,
24622459
sn.nspname AS statistics_schemaname,
@@ -2529,7 +2526,8 @@ pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
25292526
LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
25302527
LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
25312528
JOIN LATERAL ( SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
2532-
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)));
2529+
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)))
2530+
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
25332531
pg_tables| SELECT n.nspname AS schemaname,
25342532
c.relname AS tablename,
25352533
pg_get_userbyid(c.relowner) AS tableowner,

src/test/regress/expected/stats_ext.out

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3266,10 +3266,53 @@ SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not le
32663266
(0 rows)
32673267

32683268
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
3269+
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
3270+
RESET SESSION AUTHORIZATION;
3271+
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
3272+
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
3273+
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
3274+
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
3275+
ANALYZE stats_ext_tbl;
3276+
-- unprivileged role should not have access
3277+
SET SESSION AUTHORIZATION regress_stats_user1;
3278+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
3279+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3280+
statistics_name | most_common_vals
3281+
-----------------+------------------
3282+
(0 rows)
3283+
3284+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
3285+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3286+
statistics_name | most_common_vals
3287+
-----------------+------------------
3288+
(0 rows)
3289+
3290+
-- give unprivileged role ownership of table
3291+
RESET SESSION AUTHORIZATION;
3292+
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;
3293+
-- unprivileged role should now have access
3294+
SET SESSION AUTHORIZATION regress_stats_user1;
3295+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
3296+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3297+
statistics_name | most_common_vals
3298+
-----------------+-------------------------------------------
3299+
s_col | {{1,secret},{2,secret},{3,"very secret"}}
3300+
s_expr | {{0,secret},{1,secret},{1,"very secret"}}
3301+
(2 rows)
3302+
3303+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
3304+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3305+
statistics_name | most_common_vals
3306+
-----------------+------------------
3307+
s_expr | {secret}
3308+
s_expr | {1}
3309+
(2 rows)
3310+
32693311
-- Tidy up
32703312
DROP OPERATOR <<< (int, int);
32713313
DROP FUNCTION op_leak(int, int);
32723314
RESET SESSION AUTHORIZATION;
3315+
DROP TABLE stats_ext_tbl;
32733316
DROP SCHEMA tststats CASCADE;
32743317
NOTICE: drop cascades to 2 other objects
32753318
DETAIL: drop cascades to table tststats.priv_test_tbl

src/test/regress/sql/stats_ext.sql

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,9 +1649,36 @@ SET SESSION AUTHORIZATION regress_stats_user1;
16491649
SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
16501650
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
16511651

1652+
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
1653+
RESET SESSION AUTHORIZATION;
1654+
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
1655+
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
1656+
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
1657+
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
1658+
ANALYZE stats_ext_tbl;
1659+
1660+
-- unprivileged role should not have access
1661+
SET SESSION AUTHORIZATION regress_stats_user1;
1662+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
1663+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1664+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
1665+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1666+
1667+
-- give unprivileged role ownership of table
1668+
RESET SESSION AUTHORIZATION;
1669+
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;
1670+
1671+
-- unprivileged role should now have access
1672+
SET SESSION AUTHORIZATION regress_stats_user1;
1673+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
1674+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1675+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
1676+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1677+
16521678
-- Tidy up
16531679
DROP OPERATOR <<< (int, int);
16541680
DROP FUNCTION op_leak(int, int);
16551681
RESET SESSION AUTHORIZATION;
1682+
DROP TABLE stats_ext_tbl;
16561683
DROP SCHEMA tststats CASCADE;
16571684
DROP USER regress_stats_user1;

0 commit comments

Comments
 (0)