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 ('&', '|', '!') 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 );
+ } );
+ }
}