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 ) ) );
+    }
+}