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 13:52:41 UTC

(directory-ldap-api) branch master updated: Forbid infinite level of filters

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 fac89098b Forbid infinite level of filters
fac89098b is described below

commit fac89098ba22cd671bfd902e2f518aac345bd231
Author: emmanuel lecharny <el...@apache.org>
AuthorDate: Mon Sep 14 15:52:36 2026 +0200

    Forbid infinite level of filters
---
 .../java/org/apache/directory/api/i18n/I18n.java   |  1 +
 .../apache/directory/api/i18n/errors.properties    |  1 +
 .../api/ldap/model/filter/FilterParser.java        | 37 ++++++++++----
 .../api/ldap/model/filter/FilterParserTest.java    | 58 ++++++++++++++++++++++
 4 files changed, 87 insertions(+), 10 deletions(-)

diff --git a/i18n/src/main/java/org/apache/directory/api/i18n/I18n.java b/i18n/src/main/java/org/apache/directory/api/i18n/I18n.java
index 213d52c72..5d6146ff2 100644
--- a/i18n/src/main/java/org/apache/directory/api/i18n/I18n.java
+++ b/i18n/src/main/java/org/apache/directory/api/i18n/I18n.java
@@ -628,6 +628,7 @@ public enum I18n
     ERR_13316_EMPTY_FILTER( "ERR_13316_EMPTY_FILTER" ),
     ERR_13317_NO_MORE_THAN_ONE_ELEM_IN_NOT( "ERR_13317_NO_MORE_THAN_ONE_ELEM_IN_NOT" ),
     ERR_13318_INVALID_ATTR_IN_REFINEMENT( "ERR_13318_INVALID_ATTR_IN_REFINEMENT" ),
+    ERR_13319_MAX_FILTER_DEPTH_EXCEEDED( "ERR_13319_MAX_FILTER_DEPTH_EXCEEDED" ),
 
     //     ldif                         13400 - 13499
     ERR_13400_ENTRY_WITH_TWO_DNS( "ERR_13400_ENTRY_WITH_TWO_DNS" ),
diff --git a/i18n/src/main/resources/org/apache/directory/api/i18n/errors.properties b/i18n/src/main/resources/org/apache/directory/api/i18n/errors.properties
index de9294a68..a8741f7b3 100644
--- a/i18n/src/main/resources/org/apache/directory/api/i18n/errors.properties
+++ b/i18n/src/main/resources/org/apache/directory/api/i18n/errors.properties
@@ -617,6 +617,7 @@ ERR_13315_FILTER_MISSING_CLOSE_PAR=The filter has no right parenthesis
 ERR_13316_EMPTY_FILTER=Empty filter
 ERR_13317_NO_MORE_THAN_ONE_ELEM_IN_NOT=Cannot add more than one element to a negation node.
 ERR_13318_INVALID_ATTR_IN_REFINEMENT=Invalid attribute {0} for a refinement
+ERR_13319_MAX_FILTER_DEPTH_EXCEEDED=The expression exceeds the maximum allowed nesting depth ({0})
 
 # api-ldap-model ldif            13400-13499
 ERR_13400_ENTRY_WITH_TWO_DNS=An entry must not have two DNs, at line {0}
diff --git a/ldap/model/src/main/java/org/apache/directory/api/ldap/model/filter/FilterParser.java b/ldap/model/src/main/java/org/apache/directory/api/ldap/model/filter/FilterParser.java
index 272bd11fa..53779c425 100644
--- a/ldap/model/src/main/java/org/apache/directory/api/ldap/model/filter/FilterParser.java
+++ b/ldap/model/src/main/java/org/apache/directory/api/ldap/model/filter/FilterParser.java
@@ -45,6 +45,14 @@ public final class FilterParser
 {
     // The default parsing mode is STRICT
     public static final boolean STRICT = false;
+
+    /**
+     * The maximum number of nested filter levels ('&amp;', '|', '!') accepted. The parser
+     * recurses once per nesting level: without a bound, a hostile filter made of a few
+     * tens of KB of "(!" (received for instance in a referral LDAP URL) kills the calling
+     * thread with a StackOverflowError instead of a ParseException.
+     */
+    private static final int MAX_FILTER_DEPTH = 128;
     
     private FilterParser()
     {
@@ -177,7 +185,7 @@ public final class FilterParser
     {
         try
         {
-            ExprNode node = parseFilterInternal( schemaManager, filter, pos, relaxed );
+            ExprNode node = parseFilterInternal( schemaManager, filter, 0, pos, relaxed );
             
             if ( node instanceof UndefinedNode )
             {
@@ -1045,19 +1053,20 @@ public final class FilterParser
      * @param node The node to feed
      * @param filterBytes The filter bytes to parse
      * @param pos The position in the filter bytes
+     * @param depth The current filter nesting depth
      * @param relaxed If the filter is analyzed in relaxed mode or not
      * @return the created {@link ExprNode}
      * @throws ParseException If the Node can't be parsed
      * @throws LdapException If we met an error while parsing a filter element
      */
-    private static ExprNode parseBranchNode( SchemaManager schemaManager, ExprNode node, byte[] filterBytes, Position pos,
+    private static ExprNode parseBranchNode( SchemaManager schemaManager, ExprNode node, byte[] filterBytes, int depth, Position pos,
         boolean relaxed ) throws ParseException, LdapException
     {
         BranchNode branchNode = ( BranchNode ) node;
         int nbChildren = 0;
 
         // We must have at least one filter
-        ExprNode child = parseFilterInternal( schemaManager, filterBytes, pos, relaxed );
+        ExprNode child = parseFilterInternal( schemaManager, filterBytes, depth + 1, pos, relaxed );
 
         if ( !( child instanceof UndefinedNode ) )
         {
@@ -1077,7 +1086,7 @@ public final class FilterParser
         }
 
         // Now, iterate recursively though all the remaining filters, if any
-        while ( ( child = parseFilterInternal( schemaManager, filterBytes, pos, relaxed ) ) != UndefinedNode.UNDEFINED_NODE )
+        while ( ( child = parseFilterInternal( schemaManager, filterBytes, depth + 1, pos, relaxed ) ) != UndefinedNode.UNDEFINED_NODE )
         {
             // Add the child to the node children if not null
             if ( child != null )
@@ -1120,13 +1129,14 @@ public final class FilterParser
      * 
      * @param schemaManager The {@link SchemaManager}
      * @param filterBytes The filter bytes to parse
+     * @param depth The current filter nesting depth
      * @param pos The position in the filter bytes
      * @param relaxed If the filter is analyzed in relaxed mode or not
      * @return the created {@link ExprNode}
      * @throws ParseException If the Node can't be parsed
      * @throws LdapException If we met an error while parsing a filter element
      */
-    private static ExprNode parseFilterComp( SchemaManager schemaManager, byte[] filterBytes, Position pos,
+    private static ExprNode parseFilterComp( SchemaManager schemaManager, byte[] filterBytes, int depth, Position pos,
         boolean relaxed ) throws ParseException, LdapException
     {
         ExprNode node;
@@ -1148,7 +1158,7 @@ public final class FilterParser
                 skipWhiteSpaces( filterBytes, pos );
                 
                 node = new AndNode();
-                node = parseBranchNode( schemaManager, node, filterBytes, pos, relaxed );
+                node = parseBranchNode( schemaManager, node, filterBytes, depth, pos, relaxed );
                 break;
 
             case '|':
@@ -1159,7 +1169,7 @@ public final class FilterParser
                 skipWhiteSpaces( filterBytes, pos );
                 
                 node = new OrNode();
-                node = parseBranchNode( schemaManager, node, filterBytes, pos, relaxed );
+                node = parseBranchNode( schemaManager, node, filterBytes, depth, pos, relaxed );
                 break;
 
             case '!':
@@ -1170,7 +1180,7 @@ public final class FilterParser
                 skipWhiteSpaces( filterBytes, pos );
                 
                 node = new NotNode();
-                node = parseBranchNode( schemaManager, node, filterBytes, pos, relaxed );
+                node = parseBranchNode( schemaManager, node, filterBytes, depth, pos, relaxed );
                 break;
 
             default:
@@ -1191,15 +1201,22 @@ public final class FilterParser
      * 
      * @param schemaManager The {@link SchemaManager}
      * @param filterBytes The filter bytes to parse
+     * @param depth The current filter nesting depth
      * @param pos The position in the filter bytes
      * @param relaxed If the filter is analyzed in relaxed mode or not
      * @return the created {@link ExprNode}
      * @throws ParseException If the Node can't be parsed
      * @throws LdapException If we met an error while parsing a filter element
      */
-    private static ExprNode parseFilterInternal( SchemaManager schemaManager, byte[] filterBytes, Position pos,
+    private static ExprNode parseFilterInternal( SchemaManager schemaManager, byte[] filterBytes, int depth, Position pos,
         boolean relaxed ) throws ParseException, LdapException
     {
+        // Bound the recursion: a deeply nested filter is hostile, not valid
+        if ( depth > MAX_FILTER_DEPTH )
+        {
+            throw new ParseException( I18n.err( I18n.ERR_13319_MAX_FILTER_DEPTH_EXCEEDED, MAX_FILTER_DEPTH ), pos.start );
+        }
+
         // Skip spaces
         skipWhiteSpaces( filterBytes, pos );
         
@@ -1223,7 +1240,7 @@ public final class FilterParser
         skipWhiteSpaces( filterBytes, pos );
         
         // parse the filter component
-        ExprNode node = parseFilterComp( schemaManager, filterBytes, pos, relaxed );
+        ExprNode node = parseFilterComp( schemaManager, filterBytes, depth, pos, relaxed );
 
         if ( node == UndefinedNode.UNDEFINED_NODE )
         {
diff --git a/ldap/model/src/test/java/org/apache/directory/api/ldap/model/filter/FilterParserTest.java b/ldap/model/src/test/java/org/apache/directory/api/ldap/model/filter/FilterParserTest.java
index 79f9cfcfa..f88494904 100644
--- a/ldap/model/src/test/java/org/apache/directory/api/ldap/model/filter/FilterParserTest.java
+++ b/ldap/model/src/test/java/org/apache/directory/api/ldap/model/filter/FilterParserTest.java
@@ -1242,4 +1242,62 @@ public class FilterParserTest
         
         assertEquals( "(cn=ACME\\28tm\\29)", filterStr2 );
     }
+
+
+    /**
+     * A reasonably nested filter must still parse.
+     */
+    @Test
+    public void testNestedFilterWithinDepthLimit() throws ParseException
+    {
+        int depth = 50;
+        StringBuilder sb = new StringBuilder();
+
+        for ( int i = 0; i < depth; i++ )
+        {
+            sb.append( "(!" );
+        }
+
+        sb.append( "(cn=test)" );
+
+        for ( int i = 0; i < depth; i++ )
+        {
+            sb.append( ')' );
+        }
+
+        ExprNode node = FilterParser.parse( sb.toString() );
+        assertTrue( node instanceof NotNode );
+    }
+
+
+    /**
+     * A hostile, deeply nested filter (e.g. delivered in a referral LDAP URL) must be
+     * rejected with a ParseException instead of killing the calling thread with a
+     * StackOverflowError.
+     */
+    @Test
+    public void testDeeplyNestedFilterIsRejected()
+    {
+        int depth = 100000;
+        StringBuilder sb = new StringBuilder();
+
+        for ( int i = 0; i < depth; i++ )
+        {
+            sb.append( "(!" );
+        }
+
+        sb.append( "(cn=test)" );
+
+        for ( int i = 0; i < depth; i++ )
+        {
+            sb.append( ')' );
+        }
+
+        String hostile = sb.toString();
+
+        assertThrows( ParseException.class, () ->
+        {
+            FilterParser.parse( hostile );
+        } );
+    }
 }