Currently, it is pretty easy to subvert the restrictions imposed by row-level security and security_barrier views. All you have to to is use EXPLAIN (ANALYZE) and see how many rows were filtered out by the RLS policy or the view condition.
This is not considered a security bug (I asked), but I still think it should be fixed. My idea is to forbid EXPLAIN (ANALYZE) for ordinary users whenever a statement uses either of these features. But restricting it to superusers would be too restrictive (with a superuser, you can never observe RLS, since superusers are exempt) and it would also be dangerous (you shouldn't perform DML on untrusted tables as superuser). So I thought we could restrict the use of EXPLAIN (ANALYZE) in these situations to the members of a predefined role. That could be a new predefined role, but I think it might as well be "pg_read_all_stats", since that role allows you to view sensitive data like the MCV in pg_statistic, and EXPLAIN (ANALYZE) can be seen as provideing executor statistics. Attached is a POC patch that implements that (documentation and regression tests are still missing) to form a basis for a discussion. There are a few things I would like feedback about: - is it OK to use "pg_read_all_stats" for that? - should the check be moved to standard_ExplainOneQuery()? Yours, Laurenz Albe
From f31ee5919a9d30f51ff9d54adc7397cb98dfa370 Mon Sep 17 00:00:00 2001 From: Laurenz Albe <laurenz.a...@cybertec.at> Date: Mon, 6 May 2024 12:43:02 +0200 Subject: [PATCH v1] Restrict EXPLAIN (ANALYZE) on security relevant statements Using EXPLAIN (ANALYZE), it is easy to work around restrictions imposed by row-level security or security barrier views. This is not considered a security bug, but we ought to do better. Restricting the use of EXPLAIN (ANALYZE) to superusers in such cases would be too much, and superusers bypass row-level security, so that would leave no way to debug such statements. Consequently, restrict EXPLAIN (ANALYZE) on statements that involve security_barrier views or row-level security to members of the predefined role pg_read_all_stats. --- src/backend/commands/explain.c | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index c0c73aa3c9..85782e614e 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -14,6 +14,7 @@ #include "postgres.h" #include "access/xact.h" +#include "catalog/pg_authid_d.h" #include "catalog/pg_type.h" #include "commands/createas.h" #include "commands/defrem.h" @@ -21,6 +22,7 @@ #include "foreign/fdwapi.h" #include "jit/jit.h" #include "libpq/pqformat.h" +#include "miscadmin.h" #include "nodes/extensible.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" @@ -29,6 +31,7 @@ #include "rewrite/rewriteHandler.h" #include "storage/bufmgr.h" #include "tcop/tcopprot.h" +#include "utils/acl.h" #include "utils/builtins.h" #include "utils/guc_tables.h" #include "utils/json.h" @@ -431,6 +434,36 @@ ExplainOneQuery(Query *query, int cursorOptions, return; } + /* + * Since EXPLAIN (ANALYZE) shows data like the number of rows removed by a + * filter, it can be used to work around security restrictions that hide + * table data from the user, such as security barrier views and row-level + * security. Only members of pg_read_all_stats and superusers can see such + * statistics. The check is here rather than in standard_ExplainOneQuery + * to keep plugins from inadvertently subverting security. + */ + if (es->analyze && + !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + { + ListCell *rtable; + + foreach(rtable, query->rtable) + { + RangeTblEntry *rte = lfirst(rtable); + if (rte->security_barrier) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to use EXPLAIN (ANALYZE) with security_barrier views"), + errdetail("Only members of pg_read_all_stats may see statement execution statistics."))); + } + + if (query->hasRowSecurity) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to use EXPLAIN (ANALYZE) with row-level security"), + errdetail("Only members of pg_read_all_stats may see statement execution statistics."))); + } + /* if an advisor plugin is present, let it manage things */ if (ExplainOneQuery_hook) (*ExplainOneQuery_hook) (query, cursorOptions, into, es, -- 2.45.0