You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by el...@apache.org on 2026/09/14 16:28:40 UTC
(directory-ldap-api) branch master updated: Added a depth limit for Subtree specification parsing
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/directory-ldap-api.git
The following commit(s) were added to refs/heads/master by this push:
new 7da8a6a86 Added a depth limit for Subtree specification parsing
7da8a6a86 is described below
commit 7da8a6a86bf5ed9b0fc81f7c5a952eb2b79b448d
Author: emmanuel lecharny <el...@apache.org>
AuthorDate: Mon Sep 14 18:28:36 2026 +0200
Added a depth limit for Subtree specification parsing
---
.../model/subtree/SubtreeSpecificationParser.java | 47 +++++++---
.../SubtreeSpecificationParserDepthTest.java | 100 +++++++++++++++++++++
2 files changed, 134 insertions(+), 13 deletions(-)
diff --git a/ldap/model/src/main/java/org/apache/directory/api/ldap/model/subtree/SubtreeSpecificationParser.java b/ldap/model/src/main/java/org/apache/directory/api/ldap/model/subtree/SubtreeSpecificationParser.java
index f6a63590b..6beab92df 100644
--- a/ldap/model/src/main/java/org/apache/directory/api/ldap/model/subtree/SubtreeSpecificationParser.java
+++ b/ldap/model/src/main/java/org/apache/directory/api/ldap/model/subtree/SubtreeSpecificationParser.java
@@ -129,6 +129,15 @@ public class SubtreeSpecificationParser
private static final boolean PARSE = true;
private static final boolean VALIDATE = false;
+ /**
+ * The maximum number of nested refinement levels ('and', 'or', 'not') accepted.
+ * The refinement grammar is parsed by mutual recursion (parseRefinement ->
+ * parseNot/parseAndOr -> parseRefinement), one stack frame set per nesting
+ * level: without a bound, a hostile subtreeSpecification value kills the
+ * calling thread with a StackOverflowError instead of a ParseException.
+ */
+ private static final int MAX_REFINEMENT_DEPTH = 128;
+
/** The ObjectClass AT */
private static AttributeType objectClassAt;
@@ -385,18 +394,19 @@ public class SubtreeSpecificationParser
*
* @action Tells if we parse or validate the spec
* @param spec The subtreeSpecification string to parse
+ * @param depth The current depth of nested refinement
* @param pos The position in the string
* @return The parsed node
* @ParseException If the input was incorrect
*/
- private void parseAndOr( boolean action, String spec, Position pos, BranchNode node ) throws ParseException
+ private void parseAndOr( boolean action, String spec, int depth, Position pos, BranchNode node ) throws ParseException
{
// The 'and'/'or' token has already been parsed
skipSpaces( spec, pos, ZERO_N );
matchChar( spec, COLON, pos );
- parseRefinements( action, spec, pos, node );
+ parseRefinements( action, spec, depth, pos, node );
}
@@ -408,18 +418,19 @@ public class SubtreeSpecificationParser
*
* @action Tells if we parse or validate the spec
* @param spec The subtreeSpecification string to parse
+ * @param depth The current depth of nested refinement
* @param pos The position in the string
* @return The parsed node
* @ParseException If the input was incorrect
*/
- private void parseNot( boolean action, String spec, Position pos, BranchNode node ) throws ParseException
+ private void parseNot( boolean action, String spec, int depth, Position pos, BranchNode node ) throws ParseException
{
// The 'and'/'or' token has already been parsed
skipSpaces( spec, pos, ZERO_N );
matchChar( spec, COLON, pos );
- ExprNode refinement = parseRefinement( action, spec, pos );
+ ExprNode refinement = parseRefinement( action, spec, depth + 1, pos );
if ( action == PARSE )
{
@@ -439,11 +450,12 @@ public class SubtreeSpecificationParser
* </pre>
*
* @param spec The subtreeSpecification string to parse
+ * @param depth The current depth of nested refinement
* @param pos The position in the string
* @return the parsed nodes
* @ParseException If the input was incorrect
*/
- private void parseRefinements( boolean action, String spec, Position pos, BranchNode node ) throws ParseException
+ private void parseRefinements( boolean action, String spec, int depth, Position pos, BranchNode node ) throws ParseException
{
matchChar( spec, LCURLY, pos );
@@ -472,7 +484,7 @@ public class SubtreeSpecificationParser
skipSpaces( spec, pos, ZERO_N );
- ExprNode refinement = parseRefinement( action, spec, pos );
+ ExprNode refinement = parseRefinement( action, spec, depth + 1, pos );
if ( action == PARSE )
{
@@ -494,12 +506,21 @@ public class SubtreeSpecificationParser
*
* @action Tells if we parse or validate the spec
* @param spec The subtreeSpecification string to parse
+ * @param depth The current depth of nested refinement
* @param pos The position in the string
* @return the parsed refinement
* @ParseException If the input was incorrect
*/
- private ExprNode parseRefinement( boolean action, String spec, Position pos ) throws ParseException
+ private ExprNode parseRefinement( boolean action, String spec, int depth, Position pos ) throws ParseException
{
+ // Bound the recursion: a refinement nested deeper than any legitimate
+ // administrative value is hostile input, not a valid specification.
+ if ( depth > MAX_REFINEMENT_DEPTH )
+ {
+ throw new ParseException( "The subtreeSpecification refinement is nested deeper than "
+ + MAX_REFINEMENT_DEPTH + " levels", pos.start );
+ }
+
// Skip optional spaces
skipSpaces( spec, pos, ONE_N );
@@ -519,7 +540,7 @@ public class SubtreeSpecificationParser
andNode = new AndNode();
}
- parseAndOr( action, spec, pos, andNode );
+ parseAndOr( action, spec, depth, pos, andNode );
return andNode;
@@ -531,7 +552,7 @@ public class SubtreeSpecificationParser
orNode = new OrNode();
}
- parseAndOr( action, spec, pos, orNode );
+ parseAndOr( action, spec, depth, pos, orNode );
return orNode;
@@ -543,7 +564,7 @@ public class SubtreeSpecificationParser
notNode = new NotNode();
}
- parseNot( action, spec, pos, notNode );
+ parseNot( action, spec, depth, pos, notNode );
return notNode;
@@ -669,7 +690,7 @@ public class SubtreeSpecificationParser
andNode = new AndNode();
}
- parseAndOr( action, spec, pos, andNode );
+ parseAndOr( action, spec, 0, pos, andNode );
if ( action == PARSE )
{
@@ -686,7 +707,7 @@ public class SubtreeSpecificationParser
orNode = new OrNode();
}
- parseAndOr( action, spec, pos, orNode );
+ parseAndOr( action, spec, 0, pos, orNode );
if ( action == PARSE )
{
@@ -703,7 +724,7 @@ public class SubtreeSpecificationParser
new NotNode();
}
- parseNot( action, spec, pos, notNode );
+ parseNot( action, spec, 0, pos, notNode );
if ( action == PARSE )
{
diff --git a/ldap/model/src/test/java/org/apache/directory/api/ldap/model/subtree/SubtreeSpecificationParserDepthTest.java b/ldap/model/src/test/java/org/apache/directory/api/ldap/model/subtree/SubtreeSpecificationParserDepthTest.java
new file mode 100644
index 000000000..ffe16cc7d
--- /dev/null
+++ b/ldap/model/src/test/java/org/apache/directory/api/ldap/model/subtree/SubtreeSpecificationParserDepthTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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
+ *
+ * https://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.directory.api.ldap.model.subtree;
+
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.text.ParseException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Execution;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+
+
+/**
+ * Tests that the Subtree Specification parser bounds the nesting depth of the
+ * refinement grammar ('and', 'or', 'not') instead of dying with a
+ * StackOverflowError on deeply nested hostile input.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+@Execution( ExecutionMode.CONCURRENT )
+public class SubtreeSpecificationParserDepthTest
+{
+ /**
+ * Build a specificationFilter with the given number of nested 'not:' refinements.
+ */
+ private String deeplyNestedSpec( int depth )
+ {
+ StringBuilder sb = new StringBuilder( "{ specificationFilter and:{ " );
+
+ for ( int i = 0; i < depth; i++ )
+ {
+ sb.append( "not: " );
+ }
+
+ sb.append( "item:2.5.6.2 } }" );
+
+ return sb.toString();
+ }
+
+
+ /**
+ * A reasonably nested refinement must still be accepted.
+ */
+ @Test
+ public void testShallowRefinementStillParses() throws Exception
+ {
+ SubtreeSpecificationParser parser = new SubtreeSpecificationParser( null );
+
+ assertTrue( parser.check( deeplyNestedSpec( 10 ) ) );
+ }
+
+
+ /**
+ * A refinement nested beyond the maximum depth must be rejected with a
+ * ParseException, not a StackOverflowError.
+ */
+ @Test
+ public void testTooDeepRefinementIsRejected() throws Exception
+ {
+ SubtreeSpecificationParser parser = new SubtreeSpecificationParser( null );
+
+ assertThrows( ParseException.class, () -> parser.parse( deeplyNestedSpec( 200 ) ) );
+ }
+
+
+ /**
+ * A hostile, massively nested refinement must be rejected cleanly. Before the
+ * depth limit was added, this input killed the calling thread with a
+ * StackOverflowError (which check() does not catch).
+ */
+ @Test
+ public void testHostileNestingDoesNotOverflowTheStack() throws Exception
+ {
+ SubtreeSpecificationParser parser = new SubtreeSpecificationParser( null );
+
+ assertFalse( parser.check( deeplyNestedSpec( 20000 ) ) );
+ }
+}