This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/4.1.x-fixes by this push:
new 43831e7b7ce Put a configurable limit on deeply parenthesized
sub-expressions in the FiqlParser (#3442)
43831e7b7ce is described below
commit 43831e7b7ce107b5d024c16f1292490718555f9b
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 4 12:55:37 2026 +0100
Put a configurable limit on deeply parenthesized sub-expressions in the
FiqlParser (#3442)
(cherry picked from commit 8c2302b5dbd602be91342109a0ad575b2e278882)
---
.../cxf/jaxrs/ext/search/SearchContextImpl.java | 4 +-
.../cxf/jaxrs/ext/search/fiql/FiqlParser.java | 42 +++++++-
.../jaxrs/ext/search/SearchContextImplTest.java | 16 +++
.../jaxrs/ext/search/fiql/FiqlParserDepthTest.java | 108 +++++++++++++++++++++
4 files changed, 166 insertions(+), 4 deletions(-)
diff --git
a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/SearchContextImpl.java
b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/SearchContextImpl.java
index 1e8f98f2047..00124ff69d1 100644
---
a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/SearchContextImpl.java
+++
b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/SearchContextImpl.java
@@ -185,7 +185,7 @@ public class SearchContextImpl implements SearchContext {
final Map<String, String> props;
if (parserProperties == null) {
- props = new HashMap<>(5);
+ props = new HashMap<>(6);
props.put(SearchUtils.DATE_FORMAT_PROPERTY,
(String)message.getContextualProperty(SearchUtils.DATE_FORMAT_PROPERTY));
props.put(SearchUtils.TIMEZONE_SUPPORT_PROPERTY,
@@ -197,6 +197,8 @@ public class SearchContextImpl implements SearchContext {
// FIQL specific
props.put(FiqlParser.SUPPORT_SINGLE_EQUALS,
(String)message.getContextualProperty(FiqlParser.SUPPORT_SINGLE_EQUALS));
+ props.put(FiqlParser.MAX_PARENTHESIS_DEPTH,
+
(String)message.getContextualProperty(FiqlParser.MAX_PARENTHESIS_DEPTH));
} else {
props = parserProperties;
}
diff --git
a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParser.java
b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParser.java
index aa1ac058048..a71839402d0 100644
---
a/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParser.java
+++
b/rt/rs/extensions/search/src/main/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParser.java
@@ -63,9 +63,17 @@ public class FiqlParser<T> extends
AbstractSearchConditionParser<T> {
public static final String SUPPORT_SINGLE_EQUALS =
"fiql.support.single.equals.operator";
+ /**
+ * Context property limiting how deeply parenthesized sub-expressions may
nest.
+ * The parser recurses once per nesting level, so without a bound a crafted
+ * expression of thousands of nested brackets triggers a
StackOverflowError on
+ * the request thread. Must be a positive integer; the default is 64.
+ */
+ public static final String MAX_PARENTHESIS_DEPTH =
"fiql.max.parenthesis.depth";
public static final String EXTENSION_COUNT = "count";
protected static final String EXTENSION_COUNT_OPEN = EXTENSION_COUNT + "(";
+ private static final int DEFAULT_MAX_PARENTHESIS_DEPTH = 64;
private static final Map<String, ConditionType> OPERATORS_MAP;
private static final Pattern COMPARATORS_PATTERN;
private static final Pattern COMPARATORS_PATTERN_SINGLE_EQUALS;
@@ -100,6 +108,9 @@ public class FiqlParser<T> extends
AbstractSearchConditionParser<T> {
protected Map<String, ConditionType> operatorsMap = OPERATORS_MAP;
protected Pattern comparatorsPattern = COMPARATORS_PATTERN;
+
+ private final int maxParenthesisDepth;
+
/**
* Creates FIQL parser.
*
@@ -134,6 +145,8 @@ public class FiqlParser<T> extends
AbstractSearchConditionParser<T> {
Map<String, String> beanProperties) {
super(tclass, contextProperties, beanProperties);
+ this.maxParenthesisDepth =
parseMaxParenthesisDepth(this.contextProperties.get(MAX_PARENTHESIS_DEPTH));
+
if
(PropertyUtils.isTrue(this.contextProperties.get(SUPPORT_SINGLE_EQUALS))) {
operatorsMap = new HashMap<>(operatorsMap);
operatorsMap.put("=", ConditionType.EQUALS);
@@ -141,6 +154,24 @@ public class FiqlParser<T> extends
AbstractSearchConditionParser<T> {
}
}
+ private static int parseMaxParenthesisDepth(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ return DEFAULT_MAX_PARENTHESIS_DEPTH;
+ }
+ final int depth;
+ try {
+ depth = Integer.parseInt(value.trim());
+ } catch (NumberFormatException ex) {
+ throw new IllegalArgumentException(MAX_PARENTHESIS_DEPTH + " must
be a positive integer, got: "
+ + value, ex);
+ }
+ if (depth < 1) {
+ throw new IllegalArgumentException(MAX_PARENTHESIS_DEPTH + " must
be a positive integer, got: "
+ + value);
+ }
+ return depth;
+ }
+
/**
* Parses expression and builds search filter. Names used in FIQL
expression are names of getters/setters
* in type T.
@@ -165,11 +196,16 @@ public class FiqlParser<T> extends
AbstractSearchConditionParser<T> {
*/
@Override
public SearchCondition<T> parse(String fiqlExpression) throws
SearchParseException {
- ASTNode<T> ast = parseAndsOrsBrackets(fiqlExpression);
+ ASTNode<T> ast = parseAndsOrsBrackets(fiqlExpression, 0);
return ast.build();
}
- private ASTNode<T> parseAndsOrsBrackets(String expr) throws
SearchParseException {
+ private ASTNode<T> parseAndsOrsBrackets(String expr, int depth) throws
SearchParseException {
+ if (depth > maxParenthesisDepth) {
+ throw new SearchParseException("Exceeded the maximum FIQL
expression nesting depth of "
+ + maxParenthesisDepth + "; the limit can be adjusted with the "
+ + MAX_PARENTHESIS_DEPTH + " property");
+ }
List<String> subexpressions = new ArrayList<>();
List<String> operators = new ArrayList<>();
int level = 0;
@@ -226,7 +262,7 @@ public class FiqlParser<T> extends
AbstractSearchConditionParser<T> {
String subex = subexpressions.get(from);
ASTNode<T> node;
if (subex.startsWith("(")) {
- node = parseAndsOrsBrackets(subex.substring(1,
subex.length() - 1));
+ node = parseAndsOrsBrackets(subex.substring(1,
subex.length() - 1), depth + 1);
} else {
node = parseComparison(subex);
}
diff --git
a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/SearchContextImplTest.java
b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/SearchContextImplTest.java
index 32f73b29ca1..f30c9ea7b0c 100644
---
a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/SearchContextImplTest.java
+++
b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/SearchContextImplTest.java
@@ -60,6 +60,22 @@ public class SearchContextImplTest {
new SearchContextImpl(m).getCondition(Book.class);
}
+ @Test(expected = SearchParseException.class)
+ public void testMaxParenthesisDepthPropertyIsHonoured() {
+ Message m = new MessageImpl();
+ m.put(FiqlParser.MAX_PARENTHESIS_DEPTH, "1");
+ m.put(Message.QUERY_STRING, "_s=(((name==CXF)))");
+ new SearchContextImpl(m).getCondition(Book.class);
+ }
+
+ @Test
+ public void testMaxParenthesisDepthPropertyAllowsWithinBound() {
+ Message m = new MessageImpl();
+ m.put(FiqlParser.MAX_PARENTHESIS_DEPTH, "3");
+ m.put(Message.QUERY_STRING, "_s=(((name==CXF)))");
+ assertNotNull(new SearchContextImpl(m).getCondition(Book.class));
+ }
+
@Test
public void testPlainQuery2() {
Message m = new MessageImpl();
diff --git
a/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParserDepthTest.java
b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParserDepthTest.java
new file mode 100644
index 00000000000..ace3303b025
--- /dev/null
+++
b/rt/rs/extensions/search/src/test/java/org/apache/cxf/jaxrs/ext/search/fiql/FiqlParserDepthTest.java
@@ -0,0 +1,108 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.jaxrs.ext.search.fiql;
+
+import java.util.Collections;
+
+import org.apache.cxf.jaxrs.ext.search.SearchParseException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * The FIQL parser recurses once per parenthesis nesting level; nesting depth
must
+ * be bounded so a crafted expression cannot drive it into a
StackOverflowError.
+ */
+public class FiqlParserDepthTest extends Assert {
+
+ public static class Condition {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+ private static String nested(int depth) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < depth; i++) {
+ sb.append('(');
+ }
+ sb.append("name==a");
+ for (int i = 0; i < depth; i++) {
+ sb.append(')');
+ }
+ return sb.toString();
+ }
+
+ @Test
+ public void testModerateNestingParses() throws Exception {
+ FiqlParser<Condition> parser = new FiqlParser<>(Condition.class);
+ assertNotNull(parser.parse(nested(10)));
+ }
+
+ @Test
+ public void testDefaultDepthBoundaryParses() throws Exception {
+ FiqlParser<Condition> parser = new FiqlParser<>(Condition.class);
+ assertNotNull(parser.parse(nested(64)));
+ }
+
+ @Test(expected = SearchParseException.class)
+ public void testDeepNestingRejectedNotStackOverflow() throws Exception {
+ // deep enough to overflow a default thread stack if recursion were
unbounded
+ new FiqlParser<>(Condition.class).parse(nested(10000));
+ }
+
+ @Test(expected = SearchParseException.class)
+ public void testConfiguredDepthLimitEnforced() throws Exception {
+ FiqlParser<Condition> parser = new FiqlParser<>(Condition.class,
+ Collections.singletonMap(FiqlParser.MAX_PARENTHESIS_DEPTH, "2"));
+ parser.parse(nested(3));
+ }
+
+ @Test
+ public void testConfiguredDepthLimitAllowsWithinBound() throws Exception {
+ FiqlParser<Condition> parser = new FiqlParser<>(Condition.class,
+ Collections.singletonMap(FiqlParser.MAX_PARENTHESIS_DEPTH, "8"));
+ assertNotNull(parser.parse(nested(8)));
+ }
+
+ @Test
+ public void testBlankDepthPropertyFallsBackToDefault() throws Exception {
+ FiqlParser<Condition> parser = new FiqlParser<>(Condition.class,
+ Collections.singletonMap(FiqlParser.MAX_PARENTHESIS_DEPTH, " "));
+ assertNotNull(parser.parse(nested(64)));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNonNumericDepthPropertyRejected() throws Exception {
+ new FiqlParser<>(Condition.class,
+ Collections.singletonMap(FiqlParser.MAX_PARENTHESIS_DEPTH,
"lots"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNonPositiveDepthPropertyRejected() throws Exception {
+ new FiqlParser<>(Condition.class,
+ Collections.singletonMap(FiqlParser.MAX_PARENTHESIS_DEPTH, "0"));
+ }
+}