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;
+ }
}