summaryrefslogtreecommitdiff
path: root/src/backend/statistics/extended_stats.c
diff options
context:
space:
mode:
authorDean Rasheed <dean.a.rasheed@gmail.com>2025-08-11 09:09:12 +0100
committerDean Rasheed <dean.a.rasheed@gmail.com>2025-08-11 09:09:12 +0100
commita85eddab23f17bce333d7d9481f528d7ba78285e (patch)
tree688d2d86df56a8ab5ad0093cc068aae8d190edad /src/backend/statistics/extended_stats.c
parent024123a0d38e3f3fdcbab411eb01cba4b36adc86 (diff)
Fix security checks in selectivity estimation functions.
Commit e2d4ef8de86 (the fix for CVE-2017-7484) added security checks to the selectivity estimation functions to prevent them from running user-supplied operators on data obtained from pg_statistic if the user lacks privileges to select from the underlying table. In cases involving inheritance/partitioning, those checks were originally performed against the child RTE (which for plain inheritance might actually refer to the parent table). Commit 553d2ec2710 then extended that to also check the parent RTE, allowing access if the user had permissions on either the parent or the child. It turns out, however, that doing any checks using the child RTE is incorrect, since securityQuals is set to NULL when creating an RTE for an inheritance child (whether it refers to the parent table or the child table), and therefore such checks do not correctly account for any RLS policies or security barrier views. Therefore, do the security checks using only the parent RTE. This is consistent with how RLS policies are applied, and the executor's ACL checks, both of which use only the parent table's permissions/policies. Similar checks are performed in the extended stats code, so update that in the same way, centralizing all the checks in a new function. In addition, note that these checks by themselves are insufficient to ensure that the user has access to the table's data because, in a query that goes via a view, they only check that the view owner has permissions on the underlying table, not that the current user has permissions on the view itself. In the selectivity estimation functions, there is no easy way to navigate from underlying tables to views, so add permissions checks for all views mentioned in the query to the planner startup code. If the user lacks permissions on a view, a permissions error will now be reported at planner-startup, and the selectivity estimation functions will not be run. Checking view permissions at planner-startup in this way is a little ugly, since the same checks will be repeated at executor-startup. Longer-term, it might be better to move all the permissions checks from the executor to the planner so that permissions errors can be reported sooner, instead of creating a plan that won't ever be run. However, such a change seems too far-reaching to be back-patched. Back-patch to all supported versions. In v13, there is the added complication that UPDATEs and DELETEs on inherited target tables are planned using inheritance_planner(), which plans each inheritance child table separately, so that the selectivity estimation functions do not know that they are dealing with a child table accessed via its parent. Handle that by checking access permissions on the top parent table at planner-startup, in the same way as we do for views. Any securityQuals on the top parent table are moved down to the child tables by inheritance_planner(), so they continue to be checked by the selectivity estimation functions. Author: Dean Rasheed <dean.a.rasheed@gmail.com> Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us> Reviewed-by: Noah Misch <noah@leadboat.com> Backpatch-through: 13 Security: CVE-2025-8713
Diffstat (limited to 'src/backend/statistics/extended_stats.c')
-rw-r--r--src/backend/statistics/extended_stats.c108
1 files changed, 44 insertions, 64 deletions
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 99fdf208dba..b84ee2979ff 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -1320,6 +1320,9 @@ choose_best_statistics(List *stats, char requiredkind, bool inh,
* so we can't cope with system columns.
* *exprs: input/output parameter collecting primitive subclauses within
* the clause tree
+ * *leakproof: input/output parameter recording the leakproofness of the
+ * clause tree. This should be true initially, and will be set to false
+ * if any operator function used in an OpExpr is not leakproof.
*
* Returns false if there is something we definitively can't handle.
* On true return, we can proceed to match the *exprs against statistics.
@@ -1327,7 +1330,7 @@ choose_best_statistics(List *stats, char requiredkind, bool inh,
static bool
statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
Index relid, Bitmapset **attnums,
- List **exprs)
+ List **exprs, bool *leakproof)
{
/* Look inside any binary-compatible relabeling (as in examine_variable) */
if (IsA(clause, RelabelType))
@@ -1362,7 +1365,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
/* (Var/Expr op Const) or (Const op Var/Expr) */
if (is_opclause(clause))
{
- RangeTblEntry *rte = root->simple_rte_array[relid];
OpExpr *expr = (OpExpr *) clause;
Node *clause_expr;
@@ -1397,24 +1399,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
return false;
}
- /*
- * If there are any securityQuals on the RTE from security barrier
- * views or RLS policies, then the user may not have access to all the
- * table's data, and we must check that the operator is leak-proof.
- *
- * If the operator is leaky, then we must ignore this clause for the
- * purposes of estimating with MCV lists, otherwise the operator might
- * reveal values from the MCV list that the user doesn't have
- * permission to see.
- */
- if (rte->securityQuals != NIL &&
- !get_func_leakproof(get_opcode(expr->opno)))
- return false;
+ /* Check if the operator is leakproof */
+ if (*leakproof)
+ *leakproof = get_func_leakproof(get_opcode(expr->opno));
/* Check (Var op Const) or (Const op Var) clauses by recursing. */
if (IsA(clause_expr, Var))
return statext_is_compatible_clause_internal(root, clause_expr,
- relid, attnums, exprs);
+ relid, attnums,
+ exprs, leakproof);
/* Otherwise we have (Expr op Const) or (Const op Expr). */
*exprs = lappend(*exprs, clause_expr);
@@ -1424,7 +1417,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
/* Var/Expr IN Array */
if (IsA(clause, ScalarArrayOpExpr))
{
- RangeTblEntry *rte = root->simple_rte_array[relid];
ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) clause;
Node *clause_expr;
bool expronleft;
@@ -1464,24 +1456,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
return false;
}
- /*
- * If there are any securityQuals on the RTE from security barrier
- * views or RLS policies, then the user may not have access to all the
- * table's data, and we must check that the operator is leak-proof.
- *
- * If the operator is leaky, then we must ignore this clause for the
- * purposes of estimating with MCV lists, otherwise the operator might
- * reveal values from the MCV list that the user doesn't have
- * permission to see.
- */
- if (rte->securityQuals != NIL &&
- !get_func_leakproof(get_opcode(expr->opno)))
- return false;
+ /* Check if the operator is leakproof */
+ if (*leakproof)
+ *leakproof = get_func_leakproof(get_opcode(expr->opno));
/* Check Var IN Array clauses by recursing. */
if (IsA(clause_expr, Var))
return statext_is_compatible_clause_internal(root, clause_expr,
- relid, attnums, exprs);
+ relid, attnums,
+ exprs, leakproof);
/* Otherwise we have Expr IN Array. */
*exprs = lappend(*exprs, clause_expr);
@@ -1518,7 +1501,8 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
*/
if (!statext_is_compatible_clause_internal(root,
(Node *) lfirst(lc),
- relid, attnums, exprs))
+ relid, attnums, exprs,
+ leakproof))
return false;
}
@@ -1532,8 +1516,10 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
/* Check Var IS NULL clauses by recursing. */
if (IsA(nt->arg, Var))
- return statext_is_compatible_clause_internal(root, (Node *) (nt->arg),
- relid, attnums, exprs);
+ return statext_is_compatible_clause_internal(root,
+ (Node *) (nt->arg),
+ relid, attnums,
+ exprs, leakproof);
/* Otherwise we have Expr IS NULL. */
*exprs = lappend(*exprs, nt->arg);
@@ -1572,11 +1558,9 @@ static bool
statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
Bitmapset **attnums, List **exprs)
{
- RangeTblEntry *rte = root->simple_rte_array[relid];
- RelOptInfo *rel = root->simple_rel_array[relid];
RestrictInfo *rinfo;
int clause_relid;
- Oid userid;
+ bool leakproof;
/*
* Special-case handling for bare BoolExpr AND clauses, because the
@@ -1616,18 +1600,31 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
clause_relid != relid)
return false;
- /* Check the clause and determine what attributes it references. */
+ /*
+ * Check the clause, determine what attributes it references, and whether
+ * it includes any non-leakproof operators.
+ */
+ leakproof = true;
if (!statext_is_compatible_clause_internal(root, (Node *) rinfo->clause,
- relid, attnums, exprs))
+ relid, attnums, exprs,
+ &leakproof))
return false;
/*
- * Check that the user has permission to read all required attributes.
+ * If the clause includes any non-leakproof operators, check that the user
+ * has permission to read all required attributes, otherwise the operators
+ * might reveal values from the MCV list that the user doesn't have
+ * permission to see. We require all rows to be selectable --- there must
+ * be no securityQuals from security barrier views or RLS policies. See
+ * similar code in examine_variable(), examine_simple_variable(), and
+ * statistic_proc_security_check().
+ *
+ * Note that for an inheritance child, the permission checks are performed
+ * on the inheritance root parent, and whole-table select privilege on the
+ * parent doesn't guarantee that the user could read all columns of the
+ * child. Therefore we must check all referenced columns.
*/
- userid = OidIsValid(rel->userid) ? rel->userid : GetUserId();
-
- /* Table-level SELECT privilege is sufficient for all columns */
- if (pg_class_aclcheck(rte->relid, userid, ACL_SELECT) != ACLCHECK_OK)
+ if (!leakproof)
{
Bitmapset *clause_attnums = NULL;
int attnum = -1;
@@ -1652,26 +1649,9 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
if (*exprs != NIL)
pull_varattnos((Node *) *exprs, relid, &clause_attnums);
- attnum = -1;
- while ((attnum = bms_next_member(clause_attnums, attnum)) >= 0)
- {
- /* Undo the offset */
- AttrNumber attno = attnum + FirstLowInvalidHeapAttributeNumber;
-
- if (attno == InvalidAttrNumber)
- {
- /* Whole-row reference, so must have access to all columns */
- if (pg_attribute_aclcheck_all(rte->relid, userid, ACL_SELECT,
- ACLMASK_ALL) != ACLCHECK_OK)
- return false;
- }
- else
- {
- if (pg_attribute_aclcheck(rte->relid, attno, userid,
- ACL_SELECT) != ACLCHECK_OK)
- return false;
- }
- }
+ /* Must have permission to read all rows from these columns */
+ if (!all_rows_selectable(root, relid, clause_attnums))
+ return false;
}
/* If we reach here, the clause is OK */