From 6a023d62fb07692a9c374f80892cd3a18f22d3ae Mon Sep 17 00:00:00 2001 From: Mecharlance Wei Date: Tue, 31 Mar 2026 00:10:18 +0800 Subject: [PATCH] feat: harden runtime join on validation --- .../cn/beagile/dslquery/ColumnFields.java | 15 +++++++ .../java/cn/beagile/dslquery/DSLQuery.java | 1 + .../cn/beagile/dslquery/JoinOnSQLBuilder.java | 24 +++++++++++ .../java/cn/beagile/dslquery/Validators.java | 7 +++ .../cn/beagile/dslquery/JoinOnQueryTest.java | 43 +++++++++++++++++++ 5 files changed, 90 insertions(+) diff --git a/src/main/java/cn/beagile/dslquery/ColumnFields.java b/src/main/java/cn/beagile/dslquery/ColumnFields.java index 2fc15b8..6c7c0ef 100644 --- a/src/main/java/cn/beagile/dslquery/ColumnFields.java +++ b/src/main/java/cn/beagile/dslquery/ColumnFields.java @@ -22,6 +22,7 @@ public ColumnFields(DSLQuery dslQuery) { initSelectIgnores(this.clz); initDeepJoins(this.clz); readFields(this.clz); + validateRuntimeJoinOnPaths(); } private void readFields(Class clz) { @@ -84,6 +85,20 @@ private void readJoins(Class clz, List parents) { readJoinFields(clz, parents, JoinColumns.class); } + private void validateRuntimeJoinOnPaths() { + Set runtimeJoinOnPaths = Optional.ofNullable(dslQuery) + .map(DSLQuery::getJoinOns) + .map(Map::keySet) + .orElse(Collections.emptySet()); + Set joinedPaths = joinFields.stream().map(JoinField::parentNames).collect(Collectors.toSet()); + runtimeJoinOnPaths.stream() + .filter(path -> !joinedPaths.contains(path)) + .findFirst() + .ifPresent(path -> { + throw new RuntimeException("join path not found: " + path); + }); + } + private boolean isJoinInclude(Field field, List parents) { if (parents.isEmpty()) { return true; diff --git a/src/main/java/cn/beagile/dslquery/DSLQuery.java b/src/main/java/cn/beagile/dslquery/DSLQuery.java index b289d38..89ec764 100644 --- a/src/main/java/cn/beagile/dslquery/DSLQuery.java +++ b/src/main/java/cn/beagile/dslquery/DSLQuery.java @@ -132,6 +132,7 @@ public DSLQuery joinOn(String path, String dsl) { if (path == null || path.isEmpty() || dsl == null || dsl.isEmpty()) { return this; } + Validators.validateJoinPath(path); joinOns.computeIfAbsent(path, key -> new ArrayList<>()).add(dsl); return this; } diff --git a/src/main/java/cn/beagile/dslquery/JoinOnSQLBuilder.java b/src/main/java/cn/beagile/dslquery/JoinOnSQLBuilder.java index 653f988..160d7ea 100644 --- a/src/main/java/cn/beagile/dslquery/JoinOnSQLBuilder.java +++ b/src/main/java/cn/beagile/dslquery/JoinOnSQLBuilder.java @@ -13,6 +13,8 @@ import java.util.stream.Stream; class JoinOnSQLBuilder implements SQLBuilder { + private static final int MAX_JOIN_ON_EXPRESSION_LENGTH = 2048; + private static final int MAX_JOIN_ON_NESTING_DEPTH = 10; private final Map fields = new LinkedHashMap<>(); private final Map params; private final String paramPrefix; @@ -31,11 +33,33 @@ String build(List joinOns) { } WhereParser parser = new WhereParser(); return joinOns.stream() + .peek(this::validateJoinOn) .map(parser::parse) .map(expression -> expression.toSQL(this)) .collect(Collectors.joining(" and ")); } + private void validateJoinOn(String joinOn) { + if (joinOn.length() > MAX_JOIN_ON_EXPRESSION_LENGTH) { + throw new RuntimeException("join on expression too long"); + } + int depth = 0; + int maxDepth = 0; + for (int i = 0; i < joinOn.length(); i++) { + char ch = joinOn.charAt(i); + if (ch == '(') { + depth++; + maxDepth = Math.max(maxDepth, depth); + } + if (ch == ')') { + depth--; + } + } + if (maxDepth > MAX_JOIN_ON_NESTING_DEPTH) { + throw new RuntimeException("join on nesting too deep"); + } + } + private void initFields(Field joinField, List parents) { Class rootClass = parents.get(0).getDeclaringClass(); addScope("", readColumns(joinField.getType(), joinField.getType(), parents, true)); diff --git a/src/main/java/cn/beagile/dslquery/Validators.java b/src/main/java/cn/beagile/dslquery/Validators.java index 45f3a6a..60f221a 100644 --- a/src/main/java/cn/beagile/dslquery/Validators.java +++ b/src/main/java/cn/beagile/dslquery/Validators.java @@ -9,4 +9,11 @@ static void validateFieldName(String field) { throw new RuntimeException("invalid field:" + field); } } + + static void validateJoinPath(String path) { + Pattern validJoinPathPattern = Pattern.compile("[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z][a-zA-Z0-9_]*)*$"); + if (!validJoinPathPattern.matcher(path).matches()) { + throw new RuntimeException("invalid join path:" + path); + } + } } diff --git a/src/test/java/cn/beagile/dslquery/JoinOnQueryTest.java b/src/test/java/cn/beagile/dslquery/JoinOnQueryTest.java index 82d277a..8f55851 100644 --- a/src/test/java/cn/beagile/dslquery/JoinOnQueryTest.java +++ b/src/test/java/cn/beagile/dslquery/JoinOnQueryTest.java @@ -113,4 +113,47 @@ void should_fail_fast_when_join_on_field_is_unknown() { RuntimeException exception = assertThrows(RuntimeException.class, () -> sqlBuilder.build(nullsOrder)); assertEquals("field not found: missing", exception.getMessage()); } + + @Test + void should_reject_invalid_runtime_join_on_path() { + DSLQuery dslQuery = new DSLQuery<>(null, User.class); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> dslQuery.joinOn("org;drop", "(and(type eq SALES))")); + + assertEquals("invalid join path:org;drop", exception.getMessage()); + } + + @Test + void should_fail_when_runtime_join_on_path_is_not_joined() { + DSLQuery dslQuery = new DSLQuery<>(null, User.class) + .joinOn("org.area", "(and(active eq true))"); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> new DSLSQLBuilder<>(dslQuery).build(nullsOrder)); + assertEquals("join path not found: org.area", exception.getMessage()); + } + + @Test + void should_fail_when_join_on_expression_is_too_deep() { + DSLQuery dslQuery = new DSLQuery<>(null, User.class) + .joinOn("org", "(and(or(and(or(and(or(and(or(and(or(and(type eq SALES))))))))))))"); + + DSLSQLBuilder sqlBuilder = new DSLSQLBuilder<>(dslQuery); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> sqlBuilder.build(nullsOrder)); + assertEquals("join on nesting too deep", exception.getMessage()); + } + + @Test + void should_fail_when_join_on_expression_is_too_long() { + String longValue = "a".repeat(2050); + DSLQuery dslQuery = new DSLQuery<>(null, User.class) + .joinOn("org", "(and(type eq " + longValue + "))"); + + DSLSQLBuilder sqlBuilder = new DSLSQLBuilder<>(dslQuery); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> sqlBuilder.build(nullsOrder)); + assertEquals("join on expression too long", exception.getMessage()); + } }