Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions doc/src/sgml/catalogs.sgml
Original file line number Diff line number Diff line change
Expand Up @@ -7480,8 +7480,7 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
is a publicly readable view
on <structname>pg_statistic_ext_data</structname> (after joining
with <link linkend="catalog-pg-statistic-ext"><structname>pg_statistic_ext</structname></link>) that only exposes
information about those tables and columns that are readable by the
current user.
information about tables the current user owns.
</para>

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

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

Expand Down
3 changes: 2 additions & 1 deletion src/backend/catalog/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,15 @@ endif
$(INSTALL_DATA) $(srcdir)/information_schema.sql '$(DESTDIR)$(datadir)/information_schema.sql'
$(INSTALL_DATA) $(call vpathsearch,cdb_schema.sql) '$(DESTDIR)$(datadir)/cdb_init.d/cdb_schema.sql'
$(INSTALL_DATA) $(srcdir)/sql_features.txt '$(DESTDIR)$(datadir)/sql_features.txt'
$(INSTALL_DATA) $(srcdir)/fix-CVE-2024-4317.sql '$(DESTDIR)$(datadir)/fix-CVE-2024-4317.sql'

installdirs:
$(MKDIR_P) '$(DESTDIR)$(datadir)'
$(MKDIR_P) '$(DESTDIR)$(datadir)/cdb_init.d'

.PHONY: uninstall-data
uninstall-data:
rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql system_functions.sql system_views.sql system_views_gp_summary.sql information_schema.sql cdb_init.d/cdb_schema.sql cdb_init.d/gp_toolkit.sql sql_features.txt)
rm -f $(addprefix '$(DESTDIR)$(datadir)'/, postgres.bki system_constraints.sql system_functions.sql system_views.sql system_views_gp_summary.sql information_schema.sql cdb_init.d/cdb_schema.sql cdb_init.d/gp_toolkit.sql sql_features.txt fix-CVE-2024-4317.sql)
ifeq ($(USE_INTERNAL_FTS_FOUND), false)
rm -f $(addprefix '$(DESTDIR)$(datadir)'/, external_fts.sql)
endif
Expand Down
115 changes: 115 additions & 0 deletions src/backend/catalog/fix-CVE-2024-4317.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* fix-CVE-2024-4317.sql
*
* Copyright (c) 2024, PostgreSQL Global Development Group
*
* src/backend/catalog/fix-CVE-2024-4317.sql
*
* This file should be run in every database in the cluster to address
* CVE-2024-4317.
*/

SET search_path = pg_catalog;

CREATE OR REPLACE VIEW pg_stats_ext WITH (security_barrier) AS
SELECT cn.nspname AS schemaname,
c.relname AS tablename,
sn.nspname AS statistics_schemaname,
s.stxname AS statistics_name,
pg_get_userbyid(s.stxowner) AS statistics_owner,
( SELECT array_agg(a.attname ORDER BY a.attnum)
FROM unnest(s.stxkeys) k
JOIN pg_attribute a
ON (a.attrelid = s.stxrelid AND a.attnum = k)
) AS attnames,
pg_get_statisticsobjdef_expressions(s.oid) as exprs,
s.stxkind AS kinds,
sd.stxdndistinct AS n_distinct,
sd.stxddependencies AS dependencies,
m.most_common_vals,
m.most_common_val_nulls,
m.most_common_freqs,
m.most_common_base_freqs
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
LEFT JOIN LATERAL
( SELECT array_agg(values) AS most_common_vals,
array_agg(nulls) AS most_common_val_nulls,
array_agg(frequency) AS most_common_freqs,
array_agg(base_frequency) AS most_common_base_freqs
FROM pg_mcv_list_items(sd.stxdmcv)
) m ON sd.stxdmcv IS NOT NULL
WHERE pg_has_role(c.relowner, 'USAGE')
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));

CREATE OR REPLACE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
SELECT cn.nspname AS schemaname,
c.relname AS tablename,
sn.nspname AS statistics_schemaname,
s.stxname AS statistics_name,
pg_get_userbyid(s.stxowner) AS statistics_owner,
stat.expr,
(stat.a).stanullfrac AS null_frac,
(stat.a).stawidth AS avg_width,
(stat.a).stadistinct AS n_distinct,
(CASE
WHEN (stat.a).stakind1 = 1 THEN (stat.a).stavalues1
WHEN (stat.a).stakind2 = 1 THEN (stat.a).stavalues2
WHEN (stat.a).stakind3 = 1 THEN (stat.a).stavalues3
WHEN (stat.a).stakind4 = 1 THEN (stat.a).stavalues4
WHEN (stat.a).stakind5 = 1 THEN (stat.a).stavalues5
END) AS most_common_vals,
(CASE
WHEN (stat.a).stakind1 = 1 THEN (stat.a).stanumbers1
WHEN (stat.a).stakind2 = 1 THEN (stat.a).stanumbers2
WHEN (stat.a).stakind3 = 1 THEN (stat.a).stanumbers3
WHEN (stat.a).stakind4 = 1 THEN (stat.a).stanumbers4
WHEN (stat.a).stakind5 = 1 THEN (stat.a).stanumbers5
END) AS most_common_freqs,
(CASE
WHEN (stat.a).stakind1 = 2 THEN (stat.a).stavalues1
WHEN (stat.a).stakind2 = 2 THEN (stat.a).stavalues2
WHEN (stat.a).stakind3 = 2 THEN (stat.a).stavalues3
WHEN (stat.a).stakind4 = 2 THEN (stat.a).stavalues4
WHEN (stat.a).stakind5 = 2 THEN (stat.a).stavalues5
END) AS histogram_bounds,
(CASE
WHEN (stat.a).stakind1 = 3 THEN (stat.a).stanumbers1[1]
WHEN (stat.a).stakind2 = 3 THEN (stat.a).stanumbers2[1]
WHEN (stat.a).stakind3 = 3 THEN (stat.a).stanumbers3[1]
WHEN (stat.a).stakind4 = 3 THEN (stat.a).stanumbers4[1]
WHEN (stat.a).stakind5 = 3 THEN (stat.a).stanumbers5[1]
END) correlation,
(CASE
WHEN (stat.a).stakind1 = 4 THEN (stat.a).stavalues1
WHEN (stat.a).stakind2 = 4 THEN (stat.a).stavalues2
WHEN (stat.a).stakind3 = 4 THEN (stat.a).stavalues3
WHEN (stat.a).stakind4 = 4 THEN (stat.a).stavalues4
WHEN (stat.a).stakind5 = 4 THEN (stat.a).stavalues5
END) AS most_common_elems,
(CASE
WHEN (stat.a).stakind1 = 4 THEN (stat.a).stanumbers1
WHEN (stat.a).stakind2 = 4 THEN (stat.a).stanumbers2
WHEN (stat.a).stakind3 = 4 THEN (stat.a).stanumbers3
WHEN (stat.a).stakind4 = 4 THEN (stat.a).stanumbers4
WHEN (stat.a).stakind5 = 4 THEN (stat.a).stanumbers5
END) AS most_common_elem_freqs,
(CASE
WHEN (stat.a).stakind1 = 5 THEN (stat.a).stanumbers1
WHEN (stat.a).stakind2 = 5 THEN (stat.a).stanumbers2
WHEN (stat.a).stakind3 = 5 THEN (stat.a).stanumbers3
WHEN (stat.a).stakind4 = 5 THEN (stat.a).stanumbers4
WHEN (stat.a).stakind5 = 5 THEN (stat.a).stanumbers5
END) AS elem_count_histogram
FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
LEFT JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
JOIN LATERAL (
SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
unnest(sd.stxdexpr)::pg_statistic AS a
) stat ON (stat.expr IS NOT NULL)
WHERE pg_has_role(c.relowner, 'USAGE')
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
11 changes: 4 additions & 7 deletions src/backend/catalog/system_views.sql
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,7 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS
array_agg(base_frequency) AS most_common_base_freqs
FROM pg_mcv_list_items(sd.stxdmcv)
) m ON sd.stxdmcv IS NOT NULL
WHERE NOT EXISTS
( SELECT 1
FROM unnest(stxkeys) k
JOIN pg_attribute a
ON (a.attrelid = s.stxrelid AND a.attnum = k)
WHERE NOT has_column_privilege(c.oid, a.attnum, 'select') )
WHERE pg_has_role(c.relowner, 'USAGE')
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));

CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
Expand Down Expand Up @@ -384,7 +379,9 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
JOIN LATERAL (
SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
unnest(sd.stxdexpr)::pg_statistic AS a
) stat ON (stat.expr IS NOT NULL);
) stat ON (stat.expr IS NOT NULL)
WHERE pg_has_role(c.relowner, 'USAGE')
AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));

-- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data
REVOKE ALL ON pg_statistic_ext_data FROM public;
Expand Down
8 changes: 3 additions & 5 deletions src/test/regress/expected/rules.out
Original file line number Diff line number Diff line change
Expand Up @@ -2439,10 +2439,7 @@ pg_stats_ext| SELECT cn.nspname AS schemaname,
array_agg(pg_mcv_list_items.frequency) AS most_common_freqs,
array_agg(pg_mcv_list_items.base_frequency) AS most_common_base_freqs
FROM pg_mcv_list_items(sd.stxdmcv) pg_mcv_list_items(index, "values", nulls, frequency, base_frequency)) m ON ((sd.stxdmcv IS NOT NULL)))
WHERE ((NOT (EXISTS ( SELECT 1
FROM (unnest(s.stxkeys) k(k)
JOIN pg_attribute a ON (((a.attrelid = s.stxrelid) AND (a.attnum = k.k))))
WHERE (NOT has_column_privilege(c.oid, a.attnum, 'select'::text))))) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
c.relname AS tablename,
sn.nspname AS statistics_schemaname,
Expand Down Expand Up @@ -2514,7 +2511,8 @@ pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
JOIN LATERAL ( SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)));
unnest(sd.stxdexpr) AS a) stat ON ((stat.expr IS NOT NULL)))
WHERE (pg_has_role(c.relowner, 'USAGE'::text) AND ((c.relrowsecurity = false) OR (NOT row_security_active(c.oid))));
pg_tables| SELECT n.nspname AS schemaname,
c.relname AS tablename,
pg_get_userbyid(c.relowner) AS tableowner,
Expand Down
43 changes: 43 additions & 0 deletions src/test/regress/expected/stats_ext.out
Original file line number Diff line number Diff line change
Expand Up @@ -3235,10 +3235,53 @@ SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not le
(0 rows)

DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
RESET SESSION AUTHORIZATION;
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
ANALYZE stats_ext_tbl;
-- unprivileged role should not have access
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+------------------
(0 rows)

SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+------------------
(0 rows)

-- give unprivileged role ownership of table
RESET SESSION AUTHORIZATION;
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;
-- unprivileged role should now have access
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+-------------------------------------------
s_col | {{1,secret},{2,secret},{3,"very secret"}}
s_expr | {{0,secret},{1,secret},{1,"very secret"}}
(2 rows)

SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+------------------
s_expr | {secret}
s_expr | {1}
(2 rows)

-- Tidy up
DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int);
RESET SESSION AUTHORIZATION;
DROP TABLE stats_ext_tbl;
DROP SCHEMA tststats CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table tststats.priv_test_tbl
Expand Down
43 changes: 43 additions & 0 deletions src/test/regress/expected/stats_ext_optimizer.out
Original file line number Diff line number Diff line change
Expand Up @@ -3270,10 +3270,53 @@ SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not le
(0 rows)

DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
RESET SESSION AUTHORIZATION;
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
ANALYZE stats_ext_tbl;
-- unprivileged role should not have access
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+------------------
(0 rows)

SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+------------------
(0 rows)

-- give unprivileged role ownership of table
RESET SESSION AUTHORIZATION;
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;
-- unprivileged role should now have access
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+-------------------------------------------
s_col | {{1,secret},{2,secret},{3,"very secret"}}
s_expr | {{0,secret},{1,secret},{1,"very secret"}}
(2 rows)

SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
statistics_name | most_common_vals
-----------------+------------------
s_expr | {secret}
s_expr | {1}
(2 rows)

-- Tidy up
DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int);
RESET SESSION AUTHORIZATION;
DROP TABLE stats_ext_tbl;
DROP SCHEMA tststats CASCADE;
NOTICE: drop cascades to 2 other objects
DETAIL: drop cascades to table tststats.priv_test_tbl
Expand Down
27 changes: 27 additions & 0 deletions src/test/regress/sql/stats_ext.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1649,10 +1649,37 @@ SET SESSION AUTHORIZATION regress_stats_user1;
SELECT * FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak
DELETE FROM tststats.priv_test_tbl WHERE a <<< 0 AND b <<< 0; -- Should not leak

-- privilege checks for pg_stats_ext and pg_stats_ext_exprs
RESET SESSION AUTHORIZATION;
CREATE TABLE stats_ext_tbl (id INT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, col TEXT);
INSERT INTO stats_ext_tbl (col) VALUES ('secret'), ('secret'), ('very secret');
CREATE STATISTICS s_col ON id, col FROM stats_ext_tbl;
CREATE STATISTICS s_expr ON mod(id, 2), lower(col) FROM stats_ext_tbl;
ANALYZE stats_ext_tbl;

-- unprivileged role should not have access
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);

-- give unprivileged role ownership of table
RESET SESSION AUTHORIZATION;
ALTER TABLE stats_ext_tbl OWNER TO regress_stats_user1;

-- unprivileged role should now have access
SET SESSION AUTHORIZATION regress_stats_user1;
SELECT statistics_name, most_common_vals FROM pg_stats_ext x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);
SELECT statistics_name, most_common_vals FROM pg_stats_ext_exprs x
WHERE tablename = 'stats_ext_tbl' ORDER BY ROW(x.*);

-- Tidy up
DROP OPERATOR <<< (int, int);
DROP FUNCTION op_leak(int, int);
RESET SESSION AUTHORIZATION;
DROP TABLE stats_ext_tbl;
DROP SCHEMA tststats CASCADE;
DROP USER regress_stats_user1;

Expand Down
Loading