This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch tomee-10.x in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 37fd9e0abc96406262ddf90cd70d9138f84027fb Author: Markus Jung <[email protected]> AuthorDate: Sun Aug 30 20:51:01 2026 +0200 escape caller name when building ldap search filters and dns (cherry picked from commit f13948be39f8432e28c77e34b95e1b0e5e6c9cd6) --- .../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 19221704c7..c503405333 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; @@ -157,12 +158,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()); @@ -230,21 +232,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 = @@ -344,6 +347,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; + } }
