Skip to content

Commit c342538

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 0288acb commit c342538

File tree

7 files changed

+197
-17
lines changed

7 files changed

+197
-17
lines changed

doc/src/sgml/catalogs.sgml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7465,8 +7465,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
74657465
is a publicly readable view
74667466
on <structname>pg_statistic_ext_data</structname> (after joining
74677467
with <link linkend="catalog-pg-statistic-ext"><structname>pg_statistic_ext</structname></link>) that only exposes
7468-
information about those tables and columns that are readable by the
7469-
current user.
7468+
information about tables the current user owns.
74707469
</para>
74717470

74727471
<table>
@@ -12910,7 +12909,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
1291012909
and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
1291112910
catalogs. This view allows access only to rows of
1291212911
<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>
12913-
that correspond to tables the user has permission to read, and therefore
12912+
that correspond to tables the user owns, and therefore
1291412913
it is safe to allow public read access to this view.
1291512914
</para>
1291612915

@@ -13110,7 +13109,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
1311013109
and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
1311113110
catalogs. This view allows access only to rows of
1311213111
<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>
13113-
that correspond to tables the user has permission to read, and therefore
13112+
that correspond to tables the user owns, and therefore
1311413113
it is safe to allow public read access to this view.
1311513114
</para>
1311613115

src/backend/catalog/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,14 @@ install-data: bki-stamp installdirs
126126
$(INSTALL_DATA) $(srcdir)/system_views.sql '$(DESTDIR)$(datadir)/system_views.sql'
127127
$(INSTALL_DATA) $(srcdir)/information_schema.sql '$(DESTDIR)$(datadir)/information_schema.sql'
128128
$(INSTALL_DATA) $(srcdir)/sql_features.txt '$(DESTDIR)$(datadir)/sql_features.txt'
129+
$(INSTALL_DATA) $(srcdir)/fix-CVE-2024-4317.sql '$(DESTDIR)$(datadir)/fix-CVE-2024-4317.sql'
129130

130131
installdirs:
131132
$(MKDIR_P) '$(DESTDIR)$(datadir)'
132133

133134
.PHONY: uninstall-data
134135
uninstall-data:
135-
rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql system_functions.sql system_views.sql information_schema.sql sql_features.txt)
136+
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)
136137

137138
# postgres.bki, system_constraints.sql, and the generated headers are
138139
# in the distribution tarball, so they are not cleaned here.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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.stxdndistinct AS n_distinct,
28+
sd.stxddependencies AS dependencies,
29+
m.most_common_vals,
30+
m.most_common_val_nulls,
31+
m.most_common_freqs,
32+
m.most_common_base_freqs
33+
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
34+
JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
35+
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
36+
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
37+
LEFT JOIN LATERAL
38+
( SELECT array_agg(values) AS most_common_vals,
39+
array_agg(nulls) AS most_common_val_nulls,
40+
array_agg(frequency) AS most_common_freqs,
41+
array_agg(base_frequency) AS most_common_base_freqs
42+
FROM pg_mcv_list_items(sd.stxdmcv)
43+
) m ON sd.stxdmcv IS NOT NULL
44+
WHERE pg_has_role(c.relowner, 'USAGE')
45+
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
46+
47+
CREATE OR REPLACE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
48+
SELECT cn.nspname AS schemaname,
49+
c.relname AS tablename,
50+
sn.nspname AS statistics_schemaname,
51+
s.stxname AS statistics_name,
52+
pg_get_userbyid(s.stxowner) AS statistics_owner,
53+
stat.expr,
54+
(stat.a).stanullfrac AS null_frac,
55+
(stat.a).stawidth AS avg_width,
56+
(stat.a).stadistinct AS n_distinct,
57+
(CASE
58+
WHEN (stat.a).stakind1 = 1 THEN (stat.a).stavalues1
59+
WHEN (stat.a).stakind2 = 1 THEN (stat.a).stavalues2
60+
WHEN (stat.a).stakind3 = 1 THEN (stat.a).stavalues3
61+
WHEN (stat.a).stakind4 = 1 THEN (stat.a).stavalues4
62+
WHEN (stat.a).stakind5 = 1 THEN (stat.a).stavalues5
63+
END) AS most_common_vals,
64+
(CASE
65+
WHEN (stat.a).stakind1 = 1 THEN (stat.a).stanumbers1
66+
WHEN (stat.a).stakind2 = 1 THEN (stat.a).stanumbers2
67+
WHEN (stat.a).stakind3 = 1 THEN (stat.a).stanumbers3
68+
WHEN (stat.a).stakind4 = 1 THEN (stat.a).stanumbers4
69+
WHEN (stat.a).stakind5 = 1 THEN (stat.a).stanumbers5
70+
END) AS most_common_freqs,
71+
(CASE
72+
WHEN (stat.a).stakind1 = 2 THEN (stat.a).stavalues1
73+
WHEN (stat.a).stakind2 = 2 THEN (stat.a).stavalues2
74+
WHEN (stat.a).stakind3 = 2 THEN (stat.a).stavalues3
75+
WHEN (stat.a).stakind4 = 2 THEN (stat.a).stavalues4
76+
WHEN (stat.a).stakind5 = 2 THEN (stat.a).stavalues5
77+
END) AS histogram_bounds,
78+
(CASE
79+
WHEN (stat.a).stakind1 = 3 THEN (stat.a).stanumbers1[1]
80+
WHEN (stat.a).stakind2 = 3 THEN (stat.a).stanumbers2[1]
81+
WHEN (stat.a).stakind3 = 3 THEN (stat.a).stanumbers3[1]
82+
WHEN (stat.a).stakind4 = 3 THEN (stat.a).stanumbers4[1]
83+
WHEN (stat.a).stakind5 = 3 THEN (stat.a).stanumbers5[1]
84+
END) correlation,
85+
(CASE
86+
WHEN (stat.a).stakind1 = 4 THEN (stat.a).stavalues1
87+
WHEN (stat.a).stakind2 = 4 THEN (stat.a).stavalues2
88+
WHEN (stat.a).stakind3 = 4 THEN (stat.a).stavalues3
89+
WHEN (stat.a).stakind4 = 4 THEN (stat.a).stavalues4
90+
WHEN (stat.a).stakind5 = 4 THEN (stat.a).stavalues5
91+
END) AS most_common_elems,
92+
(CASE
93+
WHEN (stat.a).stakind1 = 4 THEN (stat.a).stanumbers1
94+
WHEN (stat.a).stakind2 = 4 THEN (stat.a).stanumbers2
95+
WHEN (stat.a).stakind3 = 4 THEN (stat.a).stanumbers3
96+
WHEN (stat.a).stakind4 = 4 THEN (stat.a).stanumbers4
97+
WHEN (stat.a).stakind5 = 4 THEN (stat.a).stanumbers5
98+
END) AS most_common_elem_freqs,
99+
(CASE
100+
WHEN (stat.a).stakind1 = 5 THEN (stat.a).stanumbers1
101+
WHEN (stat.a).stakind2 = 5 THEN (stat.a).stanumbers2
102+
WHEN (stat.a).stakind3 = 5 THEN (stat.a).stanumbers3
103+
WHEN (stat.a).stakind4 = 5 THEN (stat.a).stanumbers4
104+
WHEN (stat.a).stakind5 = 5 THEN (stat.a).stanumbers5
105+
END) AS elem_count_histogram
106+
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
107+
LEFT JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
108+
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
109+
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
110+
JOIN LATERAL (
111+
SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
112+
unnest(sd.stxdexpr)::pg_statistic AS a
113+
) stat ON (stat.expr IS NOT NULL)
114+
WHERE pg_has_role(c.relowner, 'USAGE')
115+
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
@@ -283,12 +283,7 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS
283283
array_agg(base_frequency) AS most_common_base_freqs
284284
FROM pg_mcv_list_items(sd.stxdmcv)
285285
) m ON sd.stxdmcv IS NOT NULL
286-
WHERE NOT EXISTS
287-
( SELECT 1
288-
FROM unnest(stxkeys) k
289-
JOIN pg_attribute a
290-
ON (a.attrelid = s.stxrelid AND a.attnum = k)
291-
WHERE NOT has_column_privilege(c.oid, a.attnum, 'select') )
286+
WHERE pg_has_role(c.relowner, 'USAGE')
292287
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
293288

294289
CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
@@ -357,7 +352,9 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
357352
JOIN LATERAL (
358353
SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
359354
unnest(sd.stxdexpr)::pg_statistic AS a
360-
) stat ON (stat.expr IS NOT NULL);
355+
) stat ON (stat.expr IS NOT NULL)
356+
WHERE pg_has_role(c.relowner, 'USAGE')
357+
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
361358

362359
-- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data
363360
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
@@ -2441,10 +2441,7 @@ pg_stats_ext| SELECT cn.nspname AS schemaname,
24412441
array_agg(pg_mcv_list_items.frequency) AS most_common_freqs,
24422442
array_agg(pg_mcv_list_items.base_frequency) AS most_common_base_freqs
24432443
FROM pg_mcv_list_items(sd.stxdmcv) pg_mcv_list_items(index, "values", nulls, frequency, base_frequency)) m ON ((sd.stxdmcv IS NOT NULL)))
2444-
WHERE ((NOT (EXISTS ( SELECT 1
2445-
FROM (unnest(s.stxkeys) k(k)
2446-
JOIN pg_attribute a ON (((a.attrelid = s.stxrelid) AND (a.attnum = k.k))))
2447-
WHERE (NOT has_column_privilege(c.oid, a.attnum, 'select'::text))))) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
2444+
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
24482445
pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
24492446
c.relname AS tablename,
24502447
sn.nspname AS statistics_schemaname,
@@ -2516,7 +2513,8 @@ pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
25162513
LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
25172514
LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
25182515
JOIN LATERAL ( SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
2519-
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)));
2516+
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)))
2517+
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
25202518
pg_tables| SELECT n.nspname AS schemaname,
25212519
c.relname AS tablename,
25222520
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
@@ -3245,10 +3245,53 @@ SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not le
32453245
(0 rows)
32463246

32473247
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
3248+
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
3249+
RESET SESSION AUTHORIZATION;
3250+
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
3251+
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
3252+
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
3253+
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
3254+
ANALYZE stats_ext_tbl;
3255+
-- unprivileged role should not have access
3256+
SET SESSION AUTHORIZATION regress_stats_user1;
3257+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
3258+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3259+
statistics_name | most_common_vals
3260+
-----------------+------------------
3261+
(0 rows)
3262+
3263+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
3264+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3265+
statistics_name | most_common_vals
3266+
-----------------+------------------
3267+
(0 rows)
3268+
3269+
-- give unprivileged role ownership of table
3270+
RESET SESSION AUTHORIZATION;
3271+
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;
3272+
-- unprivileged role should now have access
3273+
SET SESSION AUTHORIZATION regress_stats_user1;
3274+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
3275+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3276+
statistics_name | most_common_vals
3277+
-----------------+-------------------------------------------
3278+
s_col | {{1,secret},{2,secret},{3,"very secret"}}
3279+
s_expr | {{0,secret},{1,secret},{1,"very secret"}}
3280+
(2 rows)
3281+
3282+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
3283+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3284+
statistics_name | most_common_vals
3285+
-----------------+------------------
3286+
s_expr | {secret}
3287+
s_expr | {1}
3288+
(2 rows)
3289+
32483290
-- Tidy up
32493291
DROP OPERATOR <<< (int, int);
32503292
DROP FUNCTION op_leak(int, int);
32513293
RESET SESSION AUTHORIZATION;
3294+
DROP TABLE stats_ext_tbl;
32523295
DROP SCHEMA tststats CASCADE;
32533296
NOTICE: drop cascades to 2 other objects
32543297
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
@@ -1647,9 +1647,36 @@ SET SESSION AUTHORIZATION regress_stats_user1;
16471647
SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
16481648
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
16491649

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

0 commit comments

Comments
 (0)