Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 521a715

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 d1d286d commit 521a715

File tree

7 files changed

+81
-17
lines changed

7 files changed

+81
-17
lines changed

doc/src/sgml/catalogs.sgml

+1-2
Original file line numberDiff line numberDiff line change
@@ -7788,8 +7788,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
77887788
is a publicly readable view
77897789
on <structname>pg_statistic_ext_data</structname> (after joining
77907790
with <link linkend="catalog-pg-statistic-ext"><structname>pg_statistic_ext</structname></link>) that only exposes
7791-
information about those tables and columns that are readable by the
7792-
current user.
7791+
information about tables the current user owns.
77937792
</para>
77947793

77957794
<table>

doc/src/sgml/system-views.sgml

+2-2
Original file line numberDiff line numberDiff line change
@@ -3944,7 +3944,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
39443944
and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
39453945
catalogs. This view allows access only to rows of
39463946
<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>
3947-
that correspond to tables the user has permission to read, and therefore
3947+
that correspond to tables the user owns, and therefore
39483948
it is safe to allow public read access to this view.
39493949
</para>
39503950

@@ -4155,7 +4155,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
41554155
and <link linkend="catalog-pg-statistic-ext-data"><structname>pg_statistic_ext_data</structname></link>
41564156
catalogs. This view allows access only to rows of
41574157
<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>
4158-
that correspond to tables the user has permission to read, and therefore
4158+
that correspond to tables the user owns, and therefore
41594159
it is safe to allow public read access to this view.
41604160
</para>
41614161

src/backend/catalog/system_views.sql

+4-7
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,7 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS
305305
array_agg(base_frequency) AS most_common_base_freqs
306306
FROM pg_mcv_list_items(sd.stxdmcv)
307307
) m ON sd.stxdmcv IS NOT NULL
308-
WHERE NOT EXISTS
309-
( SELECT 1
310-
FROM unnest(stxkeys) k
311-
JOIN pg_attribute a
312-
ON (a.attrelid = s.stxrelid AND a.attnum = k)
313-
WHERE NOT has_column_privilege(c.oid, a.attnum, 'select') )
308+
WHERE pg_has_role(c.relowner, 'USAGE')
314309
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
315310

316311
CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
@@ -380,7 +375,9 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
380375
JOIN LATERAL (
381376
SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
382377
unnest(sd.stxdexpr)::pg_statistic AS a
383-
) stat ON (stat.expr IS NOT NULL);
378+
) stat ON (stat.expr IS NOT NULL)
379+
WHERE pg_has_role(c.relowner, 'USAGE')
380+
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
384381

385382
-- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data
386383
REVOKE ALL ON pg_statistic_ext_data FROM public;

src/include/catalog/catversion.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@
5757
*/
5858

5959
/* yyyymmddN */
60-
#define CATALOG_VERSION_NO 202405051
60+
#define CATALOG_VERSION_NO 202405061
6161

6262
#endif

src/test/regress/expected/rules.out

+3-5
Original file line numberDiff line numberDiff line change
@@ -2531,10 +2531,7 @@ pg_stats_ext| SELECT cn.nspname AS schemaname,
25312531
array_agg(pg_mcv_list_items.frequency) AS most_common_freqs,
25322532
array_agg(pg_mcv_list_items.base_frequency) AS most_common_base_freqs
25332533
FROM pg_mcv_list_items(sd.stxdmcv) pg_mcv_list_items(index, "values", nulls, frequency, base_frequency)) m ON ((sd.stxdmcv IS NOT NULL)))
2534-
WHERE ((NOT (EXISTS ( SELECT 1
2535-
FROM (unnest(s.stxkeys) k(k)
2536-
JOIN pg_attribute a ON (((a.attrelid = s.stxrelid) AND (a.attnum = k.k))))
2537-
WHERE (NOT has_column_privilege(c.oid, a.attnum, 'select'::text))))) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
2534+
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
25382535
pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
25392536
c.relname AS tablename,
25402537
sn.nspname AS statistics_schemaname,
@@ -2607,7 +2604,8 @@ pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
26072604
LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
26082605
LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
26092606
JOIN LATERAL ( SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
2610-
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)));
2607+
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)))
2608+
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
26112609
pg_tables| SELECT n.nspname AS schemaname,
26122610
c.relname AS tablename,
26132611
pg_get_userbyid(c.relowner) AS tableowner,

src/test/regress/expected/stats_ext.out

+43
Original file line numberDiff line numberDiff line change
@@ -3281,10 +3281,53 @@ SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not le
32813281
(0 rows)
32823282

32833283
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
3284+
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
3285+
RESET SESSION AUTHORIZATION;
3286+
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
3287+
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
3288+
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
3289+
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
3290+
ANALYZE stats_ext_tbl;
3291+
-- unprivileged role should not have access
3292+
SET SESSION AUTHORIZATION regress_stats_user1;
3293+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
3294+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3295+
statistics_name | most_common_vals
3296+
-----------------+------------------
3297+
(0 rows)
3298+
3299+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
3300+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3301+
statistics_name | most_common_vals
3302+
-----------------+------------------
3303+
(0 rows)
3304+
3305+
-- give unprivileged role ownership of table
3306+
RESET SESSION AUTHORIZATION;
3307+
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;
3308+
-- unprivileged role should now have access
3309+
SET SESSION AUTHORIZATION regress_stats_user1;
3310+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
3311+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3312+
statistics_name | most_common_vals
3313+
-----------------+-------------------------------------------
3314+
s_col | {{1,secret},{2,secret},{3,"very secret"}}
3315+
s_expr | {{0,secret},{1,secret},{1,"very secret"}}
3316+
(2 rows)
3317+
3318+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
3319+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
3320+
statistics_name | most_common_vals
3321+
-----------------+------------------
3322+
s_expr | {secret}
3323+
s_expr | {1}
3324+
(2 rows)
3325+
32843326
-- Tidy up
32853327
DROP OPERATOR <<< (int, int);
32863328
DROP FUNCTION op_leak(int, int);
32873329
RESET SESSION AUTHORIZATION;
3330+
DROP TABLE stats_ext_tbl;
32883331
DROP SCHEMA tststats CASCADE;
32893332
NOTICE: drop cascades to 2 other objects
32903333
DETAIL: drop cascades to table tststats.priv_test_tbl

src/test/regress/sql/stats_ext.sql

+27
Original file line numberDiff line numberDiff line change
@@ -1657,9 +1657,36 @@ SET SESSION AUTHORIZATION regress_stats_user1;
16571657
SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
16581658
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
16591659

1660+
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
1661+
RESET SESSION AUTHORIZATION;
1662+
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
1663+
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
1664+
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
1665+
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
1666+
ANALYZE stats_ext_tbl;
1667+
1668+
-- unprivileged role should not have access
1669+
SET SESSION AUTHORIZATION regress_stats_user1;
1670+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
1671+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1672+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
1673+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1674+
1675+
-- give unprivileged role ownership of table
1676+
RESET SESSION AUTHORIZATION;
1677+
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;
1678+
1679+
-- unprivileged role should now have access
1680+
SET SESSION AUTHORIZATION regress_stats_user1;
1681+
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
1682+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1683+
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
1684+
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
1685+
16601686
-- Tidy up
16611687
DROP OPERATOR <<< (int, int);
16621688
DROP FUNCTION op_leak(int, int);
16631689
RESET SESSION AUTHORIZATION;
1690+
DROP TABLE stats_ext_tbl;
16641691
DROP SCHEMA tststats CASCADE;
16651692
DROP USER regress_stats_user1;

0 commit comments

Comments
 (0)