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/08/30 22:12:41 UTC

(directory-ldap-api) branch master updated: Added some check of any attribute and matching rules injected in 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 30dd5db05 Added some check of any attribute and matching rules injected in filters
30dd5db05 is described below

commit 30dd5db05f15d78ce422dda2da28aeab4f5c0805
Author: emmanuel lecharny <el...@apache.org>
AuthorDate: Mon Aug 31 00:12:36 2026 +0200

    Added some check of any attribute and matching rules injected in filters
---
 .../java/org/apache/directory/api/i18n/I18n.java   |   2 +
 .../apache/directory/api/i18n/errors.properties    |   2 +
 .../ldap/client/api/search/AbstractFilter.java     |  75 +++++++
 .../api/search/AttributeDescriptionFilter.java     |  18 +-
 .../api/search/AttributeValueAssertionFilter.java  |  16 ++
 .../api/search/MatchingRuleAssertionFilter.java    |  33 ++-
 .../ldap/client/api/search/SubstringFilter.java    |  15 ++
 .../api/search/FilterOperandValidationTest.java    | 118 +++++++++++
 .../api/ldap/model/entry/AttributeUtils.java       | 235 ++++++++++++++++++++-
 .../api/ldap/model/filter/FilterParser.java        |   2 +-
 .../org/apache/directory/api/util/Strings.java     |  22 ++
 11 files changed, 530 insertions(+), 8 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 a9cf378f4..4f7e20844 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
@@ -219,6 +219,8 @@ public enum I18n
     ERR_04178_CANT_LOAD_KEY_STORE( "ERR_04178_CANT_LOAD_KEY_STORE" ),
     ERR_04179_TRUST_STORE_CANT_BE_READ( "ERR_04179_TRUST_STORE_CANT_BE_READ" ),
     ERR_04180_FILE_DOES_NOT_EXIST_ON_CLASSPATH( "ERR_04180_FILE_DOES_NOT_EXIST_ON_CLASSPATH" ),
+    ERR_04181_INVALID_FILTER_ATTRIBUTE_DESCRIPTIONT( "ERR_04181_INVALID_FILTER_ATTRIBUTE_DESCRIPTIONT" ),
+    ERR_04182_INVALID_MATCHING_RULE( "ERR_04182_INVALID_MATCHING_RULE" ),
 
     //     template                     4200-4300
     // None
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 3fab64105..5a180c5c2 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
@@ -209,6 +209,8 @@ ERR_04177_NO_SUCH_ALGORITHM=LdapClientTrustStoreManager.loadTrustManagers caught
 ERR_04178_CANT_LOAD_KEY_STORE=LdapClientTrustStoreManager.loadTrustManagers caught KeyStoreException
 ERR_04179_TRUST_STORE_CANT_BE_READ=LdapClientTrustStoreManager.getTrustStore finally block on input stream close operation caught IOException={0}
 ERR_04180_FILE_DOES_NOT_EXIST_ON_CLASSPATH=LdapClientTrustStoreManager.getTrustStoreInputStream file does not exist on classpath
+ERR_04181_INVALID_FILTER_ATTRIBUTE_DESCRIPTIONT=Invalid attribute description : {0}
+ERR_04182_INVALID_MATCHING_RULE: Invalid Matching Rule : {0}
 
 # api-ldap-client-api template      4200-4300
 
diff --git a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AbstractFilter.java b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AbstractFilter.java
index 58d56722c..1044d7747 100755
--- a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AbstractFilter.java
+++ b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AbstractFilter.java
@@ -19,6 +19,13 @@
  */
 package org.apache.directory.ldap.client.api.search;
 
+import java.text.ParseException;
+
+import org.apache.directory.api.i18n.I18n;
+import org.apache.directory.api.ldap.model.entry.AttributeUtils;
+import org.apache.directory.api.util.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * An abstract class used as a base for all the Filter implementations
@@ -27,6 +34,9 @@ package org.apache.directory.ldap.client.api.search;
  */
 /* No qualifier*/abstract class AbstractFilter implements Filter
 {
+    /** A logger for this class and its descendant */
+    protected static final Logger LOGGER = LoggerFactory.getLogger( Filter.class );
+    
     /**
      * {@inheritDoc}
      */
@@ -35,4 +45,69 @@ package org.apache.directory.ldap.client.api.search;
     {
         return build( new StringBuilder() );
     }
+    
+
+
+    /**
+     * Checks that a matching rule operand is a valid matching rule name
+     * (a keystring) or a numeric OID, as it is concatenated unescaped into
+     * the produced filter. A null or empty matching rule is accepted, as it
+     * simply means 'not set'.
+     *
+     * @param matchingRule The matching rule to check
+     * @return The checked matching rule
+     * @throws IllegalArgumentException If the matching rule is not valid
+     */
+    /* No qualifier*/static String checkMatchingRule( String matchingRule )
+    {
+        if ( ( matchingRule == null ) || matchingRule.isEmpty() )
+        {
+            return matchingRule;
+        }
+
+        if ( isKeyString( matchingRule ) )
+        {
+            return matchingRule;
+        }
+        else
+        {
+            try
+            {
+                AttributeUtils.parseOID( matchingRule, 0 );
+            }
+            catch ( ParseException pe )
+            {
+                throw new IllegalArgumentException( I18n.err(  I18n.ERR_04182_INVALID_MATCHING_RULE, matchingRule ) );
+            }
+
+            return matchingRule;
+        }
+    }
+
+
+    /**
+     * Checks a RFC 4512 keystrTing : leadkeychar *keychar
+     *
+     * @param str The string to check
+     * @return true if the string is a valid keystring
+     */
+    private static boolean isKeyString( String str )
+    {
+        if ( str.isEmpty() || !Strings.isAlpha( str.charAt( 0 ) ) )
+        {
+            return false;
+        }
+
+        for ( int i = 1; i < str.length(); i++ )
+        {
+            char c = str.charAt( i );
+
+            if ( !Strings.isAlpha( c ) && !Strings.isDigit( c ) && ( c != '-' ) )
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeDescriptionFilter.java b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeDescriptionFilter.java
index 156a065ae..d352b6159 100755
--- a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeDescriptionFilter.java
+++ b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeDescriptionFilter.java
@@ -19,6 +19,11 @@
  */
 package org.apache.directory.ldap.client.api.search;
 
+import java.text.ParseException;
+
+import org.apache.directory.api.i18n.I18n;
+import org.apache.directory.api.ldap.model.entry.AttributeUtils;
+import org.apache.directory.api.ldap.model.filter.FilterParser;
 
 /**
  * This class is used to handle the Present filter (ie, attr =* )
@@ -27,7 +32,7 @@ package org.apache.directory.ldap.client.api.search;
  */
 /* No qualifier*/final class AttributeDescriptionFilter extends AbstractFilter
 {
-    /** The attribute that must be prersent */
+    /** The attribute that must be present */
     private String attribute;
 
 
@@ -38,6 +43,17 @@ package org.apache.directory.ldap.client.api.search;
      */
     private AttributeDescriptionFilter( String attribute )
     {
+        // Check that the attribute is valid
+        try
+        {
+            AttributeUtils.parseAttribute( attribute, true, FilterParser.STRICT );
+        }
+        catch ( ParseException pe )
+        {
+            LOGGER.error( I18n.err( I18n.ERR_04181_INVALID_FILTER_ATTRIBUTE_DESCRIPTIONT ), attribute );
+            throw new IllegalArgumentException();
+        }
+        
         this.attribute = attribute;
     }
 
diff --git a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeValueAssertionFilter.java b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeValueAssertionFilter.java
index 1fa782cdd..da23ab294 100755
--- a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeValueAssertionFilter.java
+++ b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/AttributeValueAssertionFilter.java
@@ -20,7 +20,12 @@
 package org.apache.directory.ldap.client.api.search;
 
 
+import java.text.ParseException;
+
+import org.apache.directory.api.i18n.I18n;
+import org.apache.directory.api.ldap.model.entry.AttributeUtils;
 import org.apache.directory.api.ldap.model.filter.FilterEncoder;
+import org.apache.directory.api.ldap.model.filter.FilterParser;
 
 
 /**
@@ -49,6 +54,17 @@ import org.apache.directory.api.ldap.model.filter.FilterEncoder;
      */
     private AttributeValueAssertionFilter( String attribute, String value, FilterOperator operator )
     {
+        // Check that the attribute is valid
+        try
+        {
+            AttributeUtils.parseAttribute( attribute, true, FilterParser.STRICT );
+        }
+        catch ( ParseException pe )
+        {
+            LOGGER.error( I18n.err( I18n.ERR_04181_INVALID_FILTER_ATTRIBUTE_DESCRIPTIONT ), attribute );
+            throw new IllegalArgumentException();
+        }
+
         this.attribute = attribute;
         this.value = value;
         this.operator = operator;
diff --git a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/MatchingRuleAssertionFilter.java b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/MatchingRuleAssertionFilter.java
index b665a0b26..be541376c 100755
--- a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/MatchingRuleAssertionFilter.java
+++ b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/MatchingRuleAssertionFilter.java
@@ -20,7 +20,13 @@
 package org.apache.directory.ldap.client.api.search;
 
 
+import java.text.ParseException;
+
+import org.apache.directory.api.i18n.I18n;
+import org.apache.directory.api.ldap.model.entry.AttributeUtils;
 import org.apache.directory.api.ldap.model.filter.FilterEncoder;
+import org.apache.directory.api.ldap.model.filter.FilterParser;
+import org.apache.directory.api.util.Strings;
 
 
 /**
@@ -53,9 +59,22 @@ import org.apache.directory.api.ldap.model.filter.FilterEncoder;
      * @param value The value to test for
      * @param operator The FilterOperator
      */
-    MatchingRuleAssertionFilter( String attribute, String value, 
-        FilterOperator operator )
+    MatchingRuleAssertionFilter( String attribute, String value,  FilterOperator operator )
     {
+        // Check that the attribute is valid. It may be null
+        if ( !Strings.isEmpty( attribute ) )
+        {
+            try
+            {
+                AttributeUtils.parseAttribute( attribute, true, FilterParser.STRICT );
+            }
+            catch ( ParseException pe )
+            {
+                LOGGER.error( I18n.err( I18n.ERR_04181_INVALID_FILTER_ATTRIBUTE_DESCRIPTIONT ), attribute );
+                throw new IllegalArgumentException();
+            }
+        }
+        
         this.attribute = attribute;
         this.value = value;
         this.operator = operator;
@@ -97,7 +116,8 @@ import org.apache.directory.api.ldap.model.filter.FilterEncoder;
      */
     public MatchingRuleAssertionFilter setMatchingRule( String matchingRule )
     {
-        this.matchingRule = matchingRule;
+        this.matchingRule = checkMatchingRule( matchingRule );
+
         return this;
     }
 
@@ -109,7 +129,8 @@ import org.apache.directory.api.ldap.model.filter.FilterEncoder;
      */
     public MatchingRuleAssertionFilter useDnAttributes()
     {
-        this.useDnAttributes = true;
+        useDnAttributes = true;
+        
         return this;
     }
     
@@ -121,18 +142,22 @@ import org.apache.directory.api.ldap.model.filter.FilterEncoder;
     public StringBuilder build( StringBuilder builder )
     {
         builder.append( "(" );
+        
         if ( attribute != null )
         {
             builder.append( attribute );
         }
+        
         if ( useDnAttributes )
         {
             builder.append( ":dn" );
         }
+        
         if ( matchingRule != null && !matchingRule.isEmpty() )
         {
             builder.append( ":" ).append( matchingRule );
         }
+        
         return builder.append( operator.operator() )
             .append( FilterEncoder.encodeFilterValue( value ) ).append( ")" );
     }
diff --git a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/SubstringFilter.java b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/SubstringFilter.java
index 5eb7d30b1..98f08b8b6 100644
--- a/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/SubstringFilter.java
+++ b/ldap/client/api/src/main/java/org/apache/directory/ldap/client/api/search/SubstringFilter.java
@@ -20,10 +20,14 @@
 package org.apache.directory.ldap.client.api.search;
 
 
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.directory.api.i18n.I18n;
+import org.apache.directory.api.ldap.model.entry.AttributeUtils;
 import org.apache.directory.api.ldap.model.filter.FilterEncoder;
+import org.apache.directory.api.ldap.model.filter.FilterParser;
 import org.apache.directory.api.util.Strings;
 
 
@@ -57,6 +61,17 @@ final class SubstringFilter extends AbstractFilter
      */
     private SubstringFilter( String attribute, String initial, String[] any, String end )
     {
+        // Check that the attribute is valid
+        try
+        {
+            AttributeUtils.parseAttribute( attribute, true, FilterParser.STRICT );
+        }
+        catch ( ParseException pe )
+        {
+            LOGGER.error( I18n.err( I18n.ERR_04181_INVALID_FILTER_ATTRIBUTE_DESCRIPTIONT ), attribute );
+            throw new IllegalArgumentException();
+        }
+
         this.attribute = attribute;
         this.initial = initial;
 
diff --git a/ldap/client/api/src/test/java/org/apache/directory/ldap/client/api/search/FilterOperandValidationTest.java b/ldap/client/api/src/test/java/org/apache/directory/ldap/client/api/search/FilterOperandValidationTest.java
new file mode 100644
index 000000000..9a8f56958
--- /dev/null
+++ b/ldap/client/api/src/test/java/org/apache/directory/ldap/client/api/search/FilterOperandValidationTest.java
@@ -0,0 +1,118 @@
+/*
+ *   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.ldap.client.api.search;
+
+
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.contains;
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.equal;
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.extensible;
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.present;
+import static org.apache.directory.ldap.client.api.search.FilterBuilder.startsWith;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+
+/**
+ * Tests that no operand of the FilterBuilder API can alter the structure of
+ * the produced filter : the attribute description and matching rule operands
+ * are concatenated unescaped, so they must be validated against the RFC 4512
+ * attributedescription grammar. Otherwise an attacker-controlled 'attribute
+ * name' like "uid=admin)(roomNumber" would inject arbitrary clauses through
+ * the library's own safe-construction API.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class FilterOperandValidationTest
+{
+    @Test
+    public void testEqualRejectsFilterInjectionInAttribute()
+    {
+        assertThrows( IllegalArgumentException.class,
+            () -> equal( "uid=admin)(roomNumber", "1234" ) );
+        assertThrows( IllegalArgumentException.class,
+            () -> equal( "cn=*)(uid=admin", "x" ) );
+        assertThrows( IllegalArgumentException.class,
+            () -> equal( "", "x" ) );
+        assertThrows( IllegalArgumentException.class,
+            () -> equal( null, "x" ) );
+    }
+
+
+    @Test
+    public void testPresentRejectsFilterInjectionInAttribute()
+    {
+        assertThrows( IllegalArgumentException.class,
+            () -> present( "cn=x)(uid=admin" ) );
+    }
+
+
+    @Test
+    public void testSubstringRejectsFilterInjectionInAttribute()
+    {
+        assertThrows( IllegalArgumentException.class,
+            () -> startsWith( "cn=x)(uid=admin", "adm" ) );
+        assertThrows( IllegalArgumentException.class,
+            () -> contains( "cn=x)(uid=admin", "adm" ) );
+    }
+
+
+    @Test
+    public void testExtensibleRejectsFilterInjectionInAttribute()
+    {
+        assertThrows( IllegalArgumentException.class,
+            () -> extensible( "cn:dn:=x)(uid=admin", "x" ) );
+    }
+
+
+    @Test
+    public void testSetMatchingRuleRejectsFilterInjection()
+    {
+        assertThrows( IllegalArgumentException.class,
+            () -> extensible( "cn", "x" ).setMatchingRule( "caseExactMatch:=x)(cn=*" ) );
+        assertThrows( IllegalArgumentException.class,
+            () -> extensible( "cn", "x" ).setMatchingRule( "dn:caseExactMatch" ) );
+    }
+
+
+    @Test
+    public void testValidOperandsStillAccepted()
+    {
+        // keystring attribute
+        assertEquals( "(cn=x)", equal( "cn", "x" ).toString() );
+        
+        // Keystring with spacek
+        assertEquals( "(cn =x)", equal( "cn ", "x" ).toString() );
+
+        // numeric OID attribute
+        assertEquals( "(2.5.4.3=x)", equal( "2.5.4.3", "x" ).toString() );
+
+        // attribute description with options
+        assertEquals( "(userCertificate;binary=*)", present( "userCertificate;binary" ).toString() );
+        assertEquals( "(cn;lang-en=x)", equal( "cn;lang-en", "x" ).toString() );
+
+        // matching rule as a name and as an OID
+        assertEquals( "(cn:caseExactMatch:=x)",
+            extensible( "cn", "x" ).setMatchingRule( "caseExactMatch" ).toString() );
+        assertEquals( "(cn:2.5.13.5:=x)",
+            extensible( "cn", "x" ).setMatchingRule( "2.5.13.5" ).toString() );
+    }
+}
diff --git a/ldap/model/src/main/java/org/apache/directory/api/ldap/model/entry/AttributeUtils.java b/ldap/model/src/main/java/org/apache/directory/api/ldap/model/entry/AttributeUtils.java
index 81953b363..5680da355 100644
--- a/ldap/model/src/main/java/org/apache/directory/api/ldap/model/entry/AttributeUtils.java
+++ b/ldap/model/src/main/java/org/apache/directory/api/ldap/model/entry/AttributeUtils.java
@@ -199,6 +199,42 @@ public final class AttributeUtils
     }
 
 
+    /**
+     * Parse attribute's options :
+     * 
+     * <pre>
+     * options = *( ';' option )
+     * option = 1*keychar
+     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
+     * </pre>
+     * 
+     * @param str The parsed option
+     * @param pos The position in the parsed option string
+     * @exception ParseException The parsed option is invalid
+     */
+    private static int parseOptions( String attribute, int pos ) throws ParseException
+    {
+        while ( Strings.isCharASCII( attribute, pos, ';' ) )
+        {
+            pos++;
+
+            // We have an option
+            if ( !Chars.isAlphaDigitMinus( attribute, pos ) )
+            {
+                // We must have at least one keychar
+                throw new ParseException( I18n.err( I18n.ERR_13201_EMPTY_OPTION_NOT_ALLOWED ), pos );
+            }
+
+            pos++;
+
+            while ( Chars.isAlphaDigitMinus( attribute, pos ) )
+            {
+                pos++;
+            }
+        }
+        
+        return pos;
+    }
 
 
     /**
@@ -286,6 +322,57 @@ public final class AttributeUtils
     }
 
 
+    /**
+     * Parse a number :
+     * 
+     * <pre>
+     * number = '0' | '1'..'9' digits
+     * digits = '0'..'9'*
+     * </pre>
+     * 
+     * @param filter The number in the filter
+     * @param pos The position in the parsed filter string
+     * @return The new position if a number has been found, -1 otherwise
+     */
+    private static int parseNumber( String filter, int pos )
+    {
+        char c = Strings.charAt( filter, pos );
+
+        switch ( c )
+        {
+            case '0':
+                // If we get a starting '0', we should get out
+                pos++;
+                
+                return pos;
+
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+                pos++;
+                
+                break;
+
+            default:
+                // Not a number.
+                return -1;
+        }
+
+        while ( Chars.isDigit( filter, pos ) )
+        {
+            pos++;
+        }
+
+        return pos;
+    }
+
+
     /**
      * Parse a number :
      * 
@@ -381,8 +468,63 @@ public final class AttributeUtils
     }
 
 
+    /**
+     * Parse an OID.
+     *
+     * numericoid = number 1*( '.' number )
+     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
+     *
+     * @param str The OID to parse
+     * @param pos The current position in the string
+     * @throws ParseException If we don't have a valid OID
+     */
+    public static int parseOID( String str, int pos ) throws ParseException
+    {
+        // We have an OID
+        pos = parseNumber( str, pos );
+        
+        if ( pos == -1 )
+        {
+            throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos );
+        }
+
+        // We must have at least one '.' number
+        if ( !Strings.isCharASCII( str, pos, '.' ) )
+        {
+            throw new ParseException( I18n.err( I18n.ERR_13221_INVALID_OID_MISSING_DOT ), pos );
+        }
+
+        pos++;
 
+        pos = parseNumber( str, pos );
+        
+        if ( pos == -1 )
+        {
+            throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos );
+        }
 
+        while ( true )
+        {
+            // Break if we get something which is not a '.'
+            if ( !Strings.isCharASCII( str, pos, '.' ) )
+            {
+                break;
+            }
+
+            pos++;
+            
+            pos = parseNumber( str, pos );
+
+            if ( pos == -1 )
+            {
+                throw new ParseException( I18n.err( I18n.ERR_13202_INVALID_OID_MISSING_NUMBER ), pos );
+            }
+        }
+        
+        return pos;
+    }
+
+    
     /**
      * Parse an OID.
      *
@@ -504,8 +646,6 @@ public final class AttributeUtils
     }
 
 
-
-
     /**
      * Parse an attribute. The grammar is :
      * attributedescription = attributetype options
@@ -577,6 +717,97 @@ public final class AttributeUtils
     }
 
 
+    /**
+     * Parse an attribute. The grammar is :
+     * attributedescription = attributetype options
+     * attributetype = oid
+     * oid = descr / numericoid
+     * descr = keystring
+     * numericoid = number 1*( '.' number )
+     * options = *( ';' option )
+     * option = 1*keychar
+     * keystring = leadkeychar *keychar
+     * leadkeychar = 'a'-z' | 'A'-'Z'
+     * keychar = 'a'-z' | 'A'-'Z' / '0'-'9' / '-'
+     * number = '0'-'9' / ( '1'-'9' 1*'0'-'9' )
+     *
+     * @param bytes The parsed attribute,
+     * @param pos The position of the attribute in the current string
+     * @param withOption A flag set if we want to parse the options
+     * @param relaxed A flag set if we want to parse without being too strict
+     * @return The parsed attribute if valid
+     * @throws ParseException If we had an issue while parsing the attribute
+     */
+    public static String parseAttribute( String attribute, boolean withOption, boolean relaxed )
+        throws ParseException
+    {
+        int pos = 0;
+        
+        String trimmedAttribute = Strings.trim( attribute );
+        
+        if ( Strings.isEmpty( trimmedAttribute ) )
+        {
+            throw new ParseException( I18n.err( I18n.ERR_13222_EMPTY_ATTRIBUTE ), pos );
+        }
+        
+        // We must have an OID or an DESCR first
+        char c = attribute.charAt( pos );
+
+        if ( Chars.isAlpha( c ) )
+        {
+            pos++;
+            
+            // A DESCR
+            while ( Chars.isAlphaDigitMinus( trimmedAttribute, pos ) 
+                    || ( relaxed && Strings.isCharASCII( trimmedAttribute, pos, '_' ) ) )
+            {
+                pos++;
+            }
+
+            // Parse the options if needed
+            if ( withOption )
+            {
+                pos = parseOptions( trimmedAttribute, pos );
+            }
+
+            // Do we have trailing chars?
+            String result = trimmedAttribute.substring( 0, pos );
+            
+            if ( pos < trimmedAttribute.length() )
+            {
+                throw new ParseException( I18n.err( I18n.ERR_13223_BAD_CHAR_IN_ATTRIBUTE ), pos );
+            }
+            
+            return result;
+        }
+        else if ( Chars.isDigit( c ) )
+        {
+            // Parse the OID
+            pos = parseOID( trimmedAttribute, pos );
+
+            // Parse the options
+            if ( withOption )
+            {
+                pos = parseOptions( trimmedAttribute, pos );
+            }
+
+            // Do we have trailing chars?
+            String result = trimmedAttribute.substring( 0, pos );
+            
+            if ( pos < trimmedAttribute.length() )
+            {
+                throw new ParseException( I18n.err( I18n.ERR_13223_BAD_CHAR_IN_ATTRIBUTE ), pos );
+            }
+            
+            return result;
+        }
+        else
+        {
+            throw new ParseException( I18n.err( I18n.ERR_13223_BAD_CHAR_IN_ATTRIBUTE ), pos );
+        }
+    }
+
+
     /**
      * A method to apply a modification to an existing entry.
      * 
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 3feca02bc..272bd11fa 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
@@ -44,7 +44,7 @@ import org.apache.directory.api.util.Unicode;
 public final class FilterParser
 {
     // The default parsing mode is STRICT
-    private static final boolean STRICT = false;
+    public static final boolean STRICT = false;
     
     private FilterParser()
     {
diff --git a/util/src/main/java/org/apache/directory/api/util/Strings.java b/util/src/main/java/org/apache/directory/api/util/Strings.java
index 4ec77563a..883035215 100644
--- a/util/src/main/java/org/apache/directory/api/util/Strings.java
+++ b/util/src/main/java/org/apache/directory/api/util/Strings.java
@@ -1019,6 +1019,28 @@ public final class Strings
     }
 
 
+    /**
+     * Tells if a char is alphabetic ('a'-'z' or 'A'-'Z')
+     * @param c The char to check
+     * @return <code>true</code> if the char is alphabetic
+     */
+    public static boolean isAlpha( char c )
+    {
+        return ( ( c >= 'a' ) && ( c <= 'z' ) ) || ( ( c >= 'A' ) && ( c <= 'Z' ) );
+    }
+
+
+    /**
+     * Tells if a char is numeric ('0'-'9')
+     * @param c The char to check
+     * @return <code>true</code> if the char is numberic
+     */
+    public static boolean isDigit( char c )
+    {
+        return ( c >= '0' ) && ( c <= '9' );
+    }
+
+
     /**
      * <p>
      * Removes spaces (char &lt;= 32) from both start and ends of this String,