Skip to content

Commit a85edda

Browse files
committed
Fix security checks in selectivity estimation functions.
Commit e2d4ef8 (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 553d2ec 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
1 parent 024123a commit a85edda

File tree

12 files changed

+583
-300
lines changed

12 files changed

+583
-300
lines changed

src/backend/executor/execMain.c

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ static void ExecutePlan(QueryDesc *queryDesc,
8181
uint64 numberTuples,
8282
ScanDirection direction,
8383
DestReceiver *dest);
84-
static bool ExecCheckOneRelPerms(RTEPermissionInfo *perminfo);
8584
static bool ExecCheckPermissionsModified(Oid relOid, Oid userid,
8685
Bitmapset *modifiedCols,
8786
AclMode requiredPerms);
@@ -633,7 +632,7 @@ ExecCheckPermissions(List *rangeTable, List *rteperminfos,
633632
* ExecCheckOneRelPerms
634633
* Check access permissions for a single relation.
635634
*/
636-
static bool
635+
bool
637636
ExecCheckOneRelPerms(RTEPermissionInfo *perminfo)
638637
{
639638
AclMode requiredPerms;

src/backend/optimizer/plan/planner.c

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
#include "parser/parse_relation.h"
5959
#include "parser/parsetree.h"
6060
#include "partitioning/partdesc.h"
61+
#include "utils/acl.h"
6162
#include "utils/lsyscache.h"
6263
#include "utils/rel.h"
6364
#include "utils/selfuncs.h"
@@ -813,6 +814,38 @@ subquery_planner(PlannerGlobal *glob, Query *parse, PlannerInfo *parent_root,
813814
bms_make_singleton(parse->resultRelation);
814815
}
815816

817+
/*
818+
* This would be a convenient time to check access permissions for all
819+
* relations mentioned in the query, since it would be better to fail now,
820+
* before doing any detailed planning. However, for historical reasons,
821+
* we leave this to be done at executor startup.
822+
*
823+
* Note, however, that we do need to check access permissions for any view
824+
* relations mentioned in the query, in order to prevent information being
825+
* leaked by selectivity estimation functions, which only check view owner
826+
* permissions on underlying tables (see all_rows_selectable() and its
827+
* callers). This is a little ugly, because it means that access
828+
* permissions for views will be checked twice, which is another reason
829+
* why it would be better to do all the ACL checks here.
830+
*/
831+
foreach(l, parse->rtable)
832+
{
833+
RangeTblEntry *rte = lfirst_node(RangeTblEntry, l);
834+
835+
if (rte->perminfoindex != 0 &&
836+
rte->relkind == RELKIND_VIEW)
837+
{
838+
RTEPermissionInfo *perminfo;
839+
bool result;
840+
841+
perminfo = getRTEPermissionInfo(parse->rteperminfos, rte);
842+
result = ExecCheckOneRelPerms(perminfo);
843+
if (!result)
844+
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_VIEW,
845+
get_rel_name(perminfo->relid));
846+
}
847+
}
848+
816849
/*
817850
* Preprocess RowMark information. We need to do this after subquery
818851
* pullup, so that all base relations are present.

src/backend/statistics/extended_stats.c

Lines changed: 44 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,14 +1320,17 @@ choose_best_statistics(List *stats, char requiredkind, bool inh,
13201320
* so we can't cope with system columns.
13211321
* *exprs: input/output parameter collecting primitive subclauses within
13221322
* the clause tree
1323+
* *leakproof: input/output parameter recording the leakproofness of the
1324+
* clause tree. This should be true initially, and will be set to false
1325+
* if any operator function used in an OpExpr is not leakproof.
13231326
*
13241327
* Returns false if there is something we definitively can't handle.
13251328
* On true return, we can proceed to match the *exprs against statistics.
13261329
*/
13271330
static bool
13281331
statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
13291332
Index relid, Bitmapset **attnums,
1330-
List **exprs)
1333+
List **exprs, bool *leakproof)
13311334
{
13321335
/* Look inside any binary-compatible relabeling (as in examine_variable) */
13331336
if (IsA(clause, RelabelType))
@@ -1362,7 +1365,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
13621365
/* (Var/Expr op Const) or (Const op Var/Expr) */
13631366
if (is_opclause(clause))
13641367
{
1365-
RangeTblEntry *rte = root->simple_rte_array[relid];
13661368
OpExpr *expr = (OpExpr *) clause;
13671369
Node *clause_expr;
13681370

@@ -1397,24 +1399,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
13971399
return false;
13981400
}
13991401

1400-
/*
1401-
* If there are any securityQuals on the RTE from security barrier
1402-
* views or RLS policies, then the user may not have access to all the
1403-
* table's data, and we must check that the operator is leak-proof.
1404-
*
1405-
* If the operator is leaky, then we must ignore this clause for the
1406-
* purposes of estimating with MCV lists, otherwise the operator might
1407-
* reveal values from the MCV list that the user doesn't have
1408-
* permission to see.
1409-
*/
1410-
if (rte->securityQuals != NIL &&
1411-
!get_func_leakproof(get_opcode(expr->opno)))
1412-
return false;
1402+
/* Check if the operator is leakproof */
1403+
if (*leakproof)
1404+
*leakproof = get_func_leakproof(get_opcode(expr->opno));
14131405

14141406
/* Check (Var op Const) or (Const op Var) clauses by recursing. */
14151407
if (IsA(clause_expr, Var))
14161408
return statext_is_compatible_clause_internal(root, clause_expr,
1417-
relid, attnums, exprs);
1409+
relid, attnums,
1410+
exprs, leakproof);
14181411

14191412
/* Otherwise we have (Expr op Const) or (Const op Expr). */
14201413
*exprs = lappend(*exprs, clause_expr);
@@ -1424,7 +1417,6 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
14241417
/* Var/Expr IN Array */
14251418
if (IsA(clause, ScalarArrayOpExpr))
14261419
{
1427-
RangeTblEntry *rte = root->simple_rte_array[relid];
14281420
ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) clause;
14291421
Node *clause_expr;
14301422
bool expronleft;
@@ -1464,24 +1456,15 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
14641456
return false;
14651457
}
14661458

1467-
/*
1468-
* If there are any securityQuals on the RTE from security barrier
1469-
* views or RLS policies, then the user may not have access to all the
1470-
* table's data, and we must check that the operator is leak-proof.
1471-
*
1472-
* If the operator is leaky, then we must ignore this clause for the
1473-
* purposes of estimating with MCV lists, otherwise the operator might
1474-
* reveal values from the MCV list that the user doesn't have
1475-
* permission to see.
1476-
*/
1477-
if (rte->securityQuals != NIL &&
1478-
!get_func_leakproof(get_opcode(expr->opno)))
1479-
return false;
1459+
/* Check if the operator is leakproof */
1460+
if (*leakproof)
1461+
*leakproof = get_func_leakproof(get_opcode(expr->opno));
14801462

14811463
/* Check Var IN Array clauses by recursing. */
14821464
if (IsA(clause_expr, Var))
14831465
return statext_is_compatible_clause_internal(root, clause_expr,
1484-
relid, attnums, exprs);
1466+
relid, attnums,
1467+
exprs, leakproof);
14851468

14861469
/* Otherwise we have Expr IN Array. */
14871470
*exprs = lappend(*exprs, clause_expr);
@@ -1518,7 +1501,8 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
15181501
*/
15191502
if (!statext_is_compatible_clause_internal(root,
15201503
(Node *) lfirst(lc),
1521-
relid, attnums, exprs))
1504+
relid, attnums, exprs,
1505+
leakproof))
15221506
return false;
15231507
}
15241508

@@ -1532,8 +1516,10 @@ statext_is_compatible_clause_internal(PlannerInfo *root, Node *clause,
15321516

15331517
/* Check Var IS NULL clauses by recursing. */
15341518
if (IsA(nt->arg, Var))
1535-
return statext_is_compatible_clause_internal(root, (Node *) (nt->arg),
1536-
relid, attnums, exprs);
1519+
return statext_is_compatible_clause_internal(root,
1520+
(Node *) (nt->arg),
1521+
relid, attnums,
1522+
exprs, leakproof);
15371523

15381524
/* Otherwise we have Expr IS NULL. */
15391525
*exprs = lappend(*exprs, nt->arg);
@@ -1572,11 +1558,9 @@ static bool
15721558
statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
15731559
Bitmapset **attnums, List **exprs)
15741560
{
1575-
RangeTblEntry *rte = root->simple_rte_array[relid];
1576-
RelOptInfo *rel = root->simple_rel_array[relid];
15771561
RestrictInfo *rinfo;
15781562
int clause_relid;
1579-
Oid userid;
1563+
bool leakproof;
15801564

15811565
/*
15821566
* Special-case handling for bare BoolExpr AND clauses, because the
@@ -1616,18 +1600,31 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
16161600
clause_relid != relid)
16171601
return false;
16181602

1619-
/* Check the clause and determine what attributes it references. */
1603+
/*
1604+
* Check the clause, determine what attributes it references, and whether
1605+
* it includes any non-leakproof operators.
1606+
*/
1607+
leakproof = true;
16201608
if (!statext_is_compatible_clause_internal(root, (Node *) rinfo->clause,
1621-
relid, attnums, exprs))
1609+
relid, attnums, exprs,
1610+
&leakproof))
16221611
return false;
16231612

16241613
/*
1625-
* Check that the user has permission to read all required attributes.
1614+
* If the clause includes any non-leakproof operators, check that the user
1615+
* has permission to read all required attributes, otherwise the operators
1616+
* might reveal values from the MCV list that the user doesn't have
1617+
* permission to see. We require all rows to be selectable --- there must
1618+
* be no securityQuals from security barrier views or RLS policies. See
1619+
* similar code in examine_variable(), examine_simple_variable(), and
1620+
* statistic_proc_security_check().
1621+
*
1622+
* Note that for an inheritance child, the permission checks are performed
1623+
* on the inheritance root parent, and whole-table select privilege on the
1624+
* parent doesn't guarantee that the user could read all columns of the
1625+
* child. Therefore we must check all referenced columns.
16261626
*/
1627-
userid = OidIsValid(rel->userid) ? rel->userid : GetUserId();
1628-
1629-
/* Table-level SELECT privilege is sufficient for all columns */
1630-
if (pg_class_aclcheck(rte->relid, userid, ACL_SELECT) != ACLCHECK_OK)
1627+
if (!leakproof)
16311628
{
16321629
Bitmapset *clause_attnums = NULL;
16331630
int attnum = -1;
@@ -1652,26 +1649,9 @@ statext_is_compatible_clause(PlannerInfo *root, Node *clause, Index relid,
16521649
if (*exprs != NIL)
16531650
pull_varattnos((Node *) *exprs, relid, &clause_attnums);
16541651

1655-
attnum = -1;
1656-
while ((attnum = bms_next_member(clause_attnums, attnum)) >= 0)
1657-
{
1658-
/* Undo the offset */
1659-
AttrNumber attno = attnum + FirstLowInvalidHeapAttributeNumber;
1660-
1661-
if (attno == InvalidAttrNumber)
1662-
{
1663-
/* Whole-row reference, so must have access to all columns */
1664-
if (pg_attribute_aclcheck_all(rte->relid, userid, ACL_SELECT,
1665-
ACLMASK_ALL) != ACLCHECK_OK)
1666-
return false;
1667-
}
1668-
else
1669-
{
1670-
if (pg_attribute_aclcheck(rte->relid, attno, userid,
1671-
ACL_SELECT) != ACLCHECK_OK)
1672-
return false;
1673-
}
1674-
}
1652+
/* Must have permission to read all rows from these columns */
1653+
if (!all_rows_selectable(root, relid, clause_attnums))
1654+
return false;
16751655
}
16761656

16771657
/* If we reach here, the clause is OK */

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy