This is an automated email from the ASF dual-hosted git repository.

jungm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/main by this push:
     new f13948be39 escape caller name when building ldap search filters and dns
f13948be39 is described below

commit f13948be39f8432e28c77e34b95e1b0e5e6c9cd6
Author: Markus Jung <[email protected]>
AuthorDate: Sun Aug 30 20:51:01 2026 +0200

    escape caller name when building ldap search filters and dns
---
 .../identitystore/TomEELDAPIdentityStore.java      | 44 ++++++++--
 .../identitystore/TomEELDAPIdentityStoreTest.java  | 98 ++++++++++++++++++++++
 2 files changed, 137 insertions(+), 5 deletions(-)

diff --git 
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStore.java
 
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStore.java
index c72e2385fc..beb7c61ee5 100644
--- 
a/tomee/tomee-security/src/main/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStore.java
+++ 
b/tomee/tomee-security/src/main/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStore.java
@@ -29,6 +29,7 @@ import javax.naming.directory.SearchResult;
 import javax.naming.ldap.InitialLdapContext;
 import javax.naming.ldap.LdapContext;
 import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
 import jakarta.security.enterprise.credential.Credential;
 import jakarta.security.enterprise.credential.UsernamePasswordCredential;
 import jakarta.security.enterprise.identitystore.CredentialValidationResult;
@@ -156,12 +157,13 @@ public class TomEELDAPIdentityStore implements 
IdentityStore {
 
         } else {
 
+            final String escapedCallerDn = escapeFilterValue(callerDn);
             String filter = null;
             if (StringUtils.isNotEmpty(definition.groupSearchFilter())) {
-                filter = format(definition.groupSearchFilter(), callerDn);
+                filter = format(definition.groupSearchFilter(), 
escapedCallerDn);
 
             } else {
-                filter = format(DEFAULT_GROUP_FILTER, 
definition.groupMemberAttribute(), callerDn);
+                filter = format(DEFAULT_GROUP_FILTER, 
definition.groupMemberAttribute(), escapedCallerDn);
             }
 
             final List<SearchResult> searchResults = query(ldapContext, 
definition.groupSearchBase(), filter, getGroupSearchControls());
@@ -229,21 +231,22 @@ public class TomEELDAPIdentityStore implements 
IdentityStore {
             && StringUtils.isEmpty(definition.callerSearchBase())) {
 
             // caller DN may be provided in annotation
-            callerDn = format("%s=%s,%s", definition.callerNameAttribute(), 
callerName,
+            callerDn = format("%s=%s,%s", definition.callerNameAttribute(), 
Rdn.escapeValue(callerName),
                               definition.callerBaseDn());
 
         } else {
 
             // let's try to look it up in LDAP
+            final String escapedCallerName = escapeFilterValue(callerName);
             String filter = null;
             if (StringUtils.isNotEmpty(definition.callerSearchFilter())) {
                 filter = format(definition.callerSearchFilter(),
-                                callerName);
+                                escapedCallerName);
 
             } else {
                 filter = format(DEFAULT_USER_FILTER,
                                 definition.callerNameAttribute(),
-                                callerName);
+                                escapedCallerName);
             }
 
             final List<SearchResult> callerDns =
@@ -337,6 +340,37 @@ public class TomEELDAPIdentityStore implements 
IdentityStore {
         }
     }
 
+    // RFC 4515 filter value encoding
+    static String escapeFilterValue(final String value) {
+        if (value == null) {
+            return null;
+        }
+        final StringBuilder escaped = new StringBuilder(value.length());
+        for (int i = 0; i < value.length(); i++) {
+            final char c = value.charAt(i);
+            switch (c) {
+                case '\\':
+                    escaped.append("\\5c");
+                    break;
+                case '*':
+                    escaped.append("\\2a");
+                    break;
+                case '(':
+                    escaped.append("\\28");
+                    break;
+                case ')':
+                    escaped.append("\\29");
+                    break;
+                case '\0':
+                    escaped.append("\\00");
+                    break;
+                default:
+                    escaped.append(c);
+            }
+        }
+        return escaped.toString();
+    }
+
     private static List<SearchResult> query(
         final LdapContext ldapContext,
         final String base,
diff --git 
a/tomee/tomee-security/src/test/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStoreTest.java
 
b/tomee/tomee-security/src/test/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStoreTest.java
index 4c8de4892a..571d45c074 100644
--- 
a/tomee/tomee-security/src/test/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStoreTest.java
+++ 
b/tomee/tomee-security/src/test/java/org/apache/tomee/security/identitystore/TomEELDAPIdentityStoreTest.java
@@ -19,14 +19,25 @@ package org.apache.tomee.security.identitystore;
 import jakarta.security.enterprise.credential.UsernamePasswordCredential;
 import jakarta.security.enterprise.identitystore.LdapIdentityStoreDefinition;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapContext;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 public class TomEELDAPIdentityStoreTest {
 
@@ -49,4 +60,91 @@ public class TomEELDAPIdentityStoreTest {
         assertFalse(result);
         verify(definition, never()).url();
     }
+
+    @Test
+    public void escapeFilterValue() {
+        assertEquals("plainUser", 
TomEELDAPIdentityStore.escapeFilterValue("plainUser"));
+        assertEquals("a\\2a", TomEELDAPIdentityStore.escapeFilterValue("a*"));
+        assertEquals("back\\5cslash", 
TomEELDAPIdentityStore.escapeFilterValue("back\\slash"));
+        assertEquals("nul\\00byte", 
TomEELDAPIdentityStore.escapeFilterValue("nul\0byte"));
+        assertEquals("\\2a\\29\\28objectclass=\\2a", 
TomEELDAPIdentityStore.escapeFilterValue("*)(objectclass=*"));
+        assertNull(TomEELDAPIdentityStore.escapeFilterValue(null));
+    }
+
+    @Test
+    public void callerNameIsEscapedInSearchFilter() throws Exception {
+        final LdapIdentityStoreDefinition definition = 
mock(LdapIdentityStoreDefinition.class);
+        
when(definition.callerSearchBase()).thenReturn("ou=people,dc=example,dc=org");
+        when(definition.callerNameAttribute()).thenReturn("uid");
+        
when(definition.callerSearchScope()).thenReturn(LdapIdentityStoreDefinition.LdapSearchScope.SUBTREE);
+        when(definition.callerSearchFilter()).thenReturn("");
+
+        final NamingEnumeration<SearchResult> noResults = emptyEnumeration();
+        final LdapContext ldapContext = mock(LdapContext.class);
+        when(ldapContext.search(anyString(), anyString(), 
any(SearchControls.class))).thenReturn(noResults);
+
+        final String filter = searchFilterFor(definition, ldapContext, 
"user)(objectclass=*");
+        
assertEquals("(&(uid=user\\29\\28objectclass=\\2a)(|(objectclass=user)(objectclass=person)(objectclass=inetOrgPerson)(objectclass=organizationalPerson))(!(objectclass=computer)))",
 filter);
+    }
+
+    @Test
+    public void callerNameIsEscapedInCustomSearchFilter() throws Exception {
+        final LdapIdentityStoreDefinition definition = 
mock(LdapIdentityStoreDefinition.class);
+        
when(definition.callerSearchBase()).thenReturn("ou=people,dc=example,dc=org");
+        when(definition.callerNameAttribute()).thenReturn("uid");
+        
when(definition.callerSearchScope()).thenReturn(LdapIdentityStoreDefinition.LdapSearchScope.SUBTREE);
+        when(definition.callerSearchFilter()).thenReturn("(cn=%s)");
+
+        final NamingEnumeration<SearchResult> noResults = emptyEnumeration();
+        final LdapContext ldapContext = mock(LdapContext.class);
+        when(ldapContext.search(anyString(), anyString(), 
any(SearchControls.class))).thenReturn(noResults);
+
+        final String filter = searchFilterFor(definition, ldapContext, "a*");
+        assertEquals("(cn=a\\2a)", filter);
+    }
+
+    @Test
+    public void callerNameIsEscapedInCallerDn() throws Exception {
+        final LdapIdentityStoreDefinition definition = 
mock(LdapIdentityStoreDefinition.class);
+        
when(definition.callerBaseDn()).thenReturn("ou=people,dc=example,dc=org");
+        when(definition.callerNameAttribute()).thenReturn("uid");
+        when(definition.callerSearchBase()).thenReturn("");
+
+        final TomEELDAPIdentityStore store = storeWith(definition);
+        final Method getCallerDn = 
TomEELDAPIdentityStore.class.getDeclaredMethod(
+            "getCallerDn", LdapContext.class, String.class);
+        getCallerDn.setAccessible(true);
+
+        final String callerDn = (String) getCallerDn.invoke(store, 
mock(LdapContext.class), "user,ou=admins");
+        assertEquals("uid=user\\,ou\\=admins,ou=people,dc=example,dc=org", 
callerDn);
+    }
+
+    private static String searchFilterFor(final LdapIdentityStoreDefinition 
definition,
+                                          final LdapContext ldapContext,
+                                          final String callerName) throws 
Exception {
+        final TomEELDAPIdentityStore store = storeWith(definition);
+        final Method getCallerDn = 
TomEELDAPIdentityStore.class.getDeclaredMethod(
+            "getCallerDn", LdapContext.class, String.class);
+        getCallerDn.setAccessible(true);
+        getCallerDn.invoke(store, ldapContext, callerName);
+
+        final ArgumentCaptor<String> filter = 
ArgumentCaptor.forClass(String.class);
+        verify(ldapContext).search(eq("ou=people,dc=example,dc=org"), 
filter.capture(), any(SearchControls.class));
+        return filter.getValue();
+    }
+
+    private static TomEELDAPIdentityStore storeWith(final 
LdapIdentityStoreDefinition definition) throws Exception {
+        final TomEELDAPIdentityStore store = new TomEELDAPIdentityStore();
+        final Field definitionField = 
TomEELDAPIdentityStore.class.getDeclaredField("definition");
+        definitionField.setAccessible(true);
+        definitionField.set(store, definition);
+        return store;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static NamingEnumeration<SearchResult> emptyEnumeration() {
+        final NamingEnumeration<SearchResult> enumeration = 
mock(NamingEnumeration.class);
+        when(enumeration.hasMoreElements()).thenReturn(false);
+        return enumeration;
+    }
 }

Reply via email to