This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 98dec70384d [fix](fe) Fix Ranger column-level privilege bypass when
CTE combined (#61741)
98dec70384d is described below
commit 98dec70384df1c8baa82fc9aff4ed3c785a97a11
Author: smith1000 <[email protected]>
AuthorDate: Thu May 7 11:27:10 2026 +0800
[fix](fe) Fix Ranger column-level privilege bypass when CTE combined
(#61741)
### What problem does this PR solve?
Issue Number: close #61631
Problem Summary: When a CTE (WITH ... AS) is referenced multiple times
in a
JOIN query and is not inlined (due to inlineCTEReferencedThreshold), the
CheckPrivileges rule does not traverse the CTE producer subtree because
LogicalCTEConsumer is a leaf node in the plan tree. This means
column-level
privileges on the CTE's underlying tables are never checked, allowing
users
without proper column access to bypass Ranger authorization.
The fix adds a `visitLogicalCTEConsumer` override in `CheckPrivileges`
that
explicitly retrieves the CTE producer plan (stored by
`RewriteCteChildren`)
and traverses it for privilege checking. The `privChecked` flag remains
on
`StatementContext` to preserve the view permission passthrough
mechanism.
### Release note
Fixed a security issue where Ranger column-level privileges could be
bypassed
when using CTE (WITH ... AS) combined with JOIN queries. Users without
proper
column access permissions could read restricted columns through CTE+JOIN
patterns.
### Check List (For Author)
- Test: Unit Test / Manual test (verified with Ranger 2.7.0 + Doris
4.0.2 environment)
- Behavior changed: No
- Does this need documentation: No
---------
Co-authored-by: geshengli <[email protected]>
---
.../nereids/rules/rewrite/CheckPrivileges.java | 33 +++++++++++++++++++---
.../nereids/privileges/TestCheckPrivileges.java | 27 ++++++++++++++++++
2 files changed, 56 insertions(+), 4 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckPrivileges.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckPrivileges.java
index b65f7245ca7..bce4862aeac 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckPrivileges.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/CheckPrivileges.java
@@ -25,19 +25,23 @@ import org.apache.doris.nereids.StatementContext;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.jobs.JobContext;
import org.apache.doris.nereids.rules.analysis.UserAuthentication;
+import org.apache.doris.nereids.trees.expressions.CTEId;
import org.apache.doris.nereids.trees.expressions.Slot;
import org.apache.doris.nereids.trees.expressions.SlotReference;
import
org.apache.doris.nereids.trees.expressions.functions.table.TableValuedFunction;
import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalCTEConsumer;
import org.apache.doris.nereids.trees.plans.logical.LogicalCatalogRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalTVFRelation;
import org.apache.doris.nereids.trees.plans.logical.LogicalView;
import org.apache.doris.qe.ConnectContext;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import org.roaringbitmap.RoaringBitmap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -46,20 +50,28 @@ import java.util.Set;
/**
* CheckPrivileges
- * This rule should only check once, because after check would set
setPrivChecked in statementContext
+ *
+ * The privChecked flag is on StatementContext to support view permission
passthrough:
+ * when InlineLogicalView expands a view, the outer CheckPrivileges has
already verified
+ * the view-level permission, so the inner tables should not be re-checked.
+ *
+ * For CTEs, since LogicalCTEConsumer is a leaf node and the producer subtree
is not
+ * traversed by ColumnPruning, we override visitLogicalCTEConsumer to
explicitly
+ * traverse the producer plan and check privileges on its tables.
*/
public class CheckPrivileges extends ColumnPruning {
private JobContext jobContext;
+ private final Set<CTEId> checkedCteIds = new HashSet<>();
@Override
public Plan rewriteRoot(Plan plan, JobContext jobContext) {
- // Only enter once, if repeated, the permissions of the table in the
view will be checked
- if
(jobContext.getCascadesContext().getStatementContext().isPrivChecked()) {
+ StatementContext stmtCtx =
jobContext.getCascadesContext().getStatementContext();
+ if (stmtCtx.isPrivChecked()) {
return plan;
}
this.jobContext = jobContext;
super.rewriteRoot(plan, jobContext);
-
jobContext.getCascadesContext().getStatementContext().setPrivChecked(true);
+ stmtCtx.setPrivChecked(true);
// don't rewrite plan, because Reorder expect no LogicalProject on
LogicalJoin
return plan;
}
@@ -88,6 +100,19 @@ public class CheckPrivileges extends ColumnPruning {
return super.visitLogicalRelation(relation, context);
}
+ @Override
+ public Plan visitLogicalCTEConsumer(LogicalCTEConsumer consumer,
PruneContext context) {
+ CTEId cteId = consumer.getCteId();
+ StatementContext stmtCtx =
jobContext.getCascadesContext().getStatementContext();
+ Plan producerPlan = stmtCtx.getCteProducerByCteId(cteId);
+ if (producerPlan != null && checkedCteIds.add(cteId)) {
+ PruneContext producerContext = new PruneContext(
+ null, producerPlan.getOutputExprIdBitSet(),
ImmutableList.of(), true);
+ producerPlan.accept(this, producerContext);
+ }
+ return super.visitLogicalCTEConsumer(consumer, context);
+ }
+
private Set<String> computeUsedColumns(Plan plan, RoaringBitmap
requiredSlotIds) {
List<Slot> outputs = plan.getOutput();
Map<Integer, Slot> idToSlot = new LinkedHashMap<>(outputs.size());
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java
index 658f3a96128..0473bce3250 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/privileges/TestCheckPrivileges.java
@@ -163,6 +163,33 @@ public class TestCheckPrivileges extends TestWithFeService
implements GeneratedM
);
}
+ // test CTE with JOIN privilege checking
+ // Verifies that column-level privileges are enforced when CTE
is
+ // referenced multiple times via JOIN (CTE won't be inlined
due to
+ // inlineCTEReferencedThreshold).
CheckPrivileges.visitLogicalCTEConsumer
+ // explicitly traverses the CTE producer plan to check
privileges.
+ {
+ // CTE + JOIN on fully-privileged table should succeed
+ query("WITH cte AS (SELECT id, name FROM
custom_catalog.test_db.test_tbl1) "
+ + "SELECT a.id FROM cte a LEFT JOIN cte b ON a.id
= b.id");
+
+ // CTE + JOIN accessing restricted column should be denied
+ Assertions.assertThrows(AnalysisException.class, () ->
+ query("WITH cte AS (SELECT * FROM
custom_catalog.test_db.test_tbl2) "
+ + "SELECT a.id FROM cte a LEFT JOIN cte b
ON a.id = b.id")
+ );
+
+ // CTE + JOIN accessing only allowed columns should succeed
+ query("WITH cte AS (SELECT id FROM
custom_catalog.test_db.test_tbl2) "
+ + "SELECT a.id FROM cte a LEFT JOIN cte b ON a.id
= b.id");
+
+ // CTE + INNER JOIN accessing restricted column should
also be denied
+ Assertions.assertThrows(AnalysisException.class, () ->
+ query("WITH cte AS (SELECT * FROM
custom_catalog.test_db.test_tbl2) "
+ + "SELECT a.id FROM cte a INNER JOIN cte b
ON a.id = b.id")
+ );
+ }
+
// test row policy with data masking
{
Function<NamedExpression, Boolean> checkId =
(NamedExpression ne) -> {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]