You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by ha...@apache.org on 2026/08/14 06:12:52 UTC

(knox) 02/02: KNOX-3417: KnoxLdapRealm builds the LDAP search filter from the client username without RFC-4515 escaping (#1348)

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

hanicz pushed a commit to branch v3.0.0
in repository https://gitbox.apache.org/repos/asf/knox.git

commit cc6b674e33f47d90b7600adab3bb2f181b4e7691
Author: hanicz <ha...@users.noreply.github.com>
AuthorDate: Fri Aug 14 08:10:30 2026 +0200

    KNOX-3417: KnoxLdapRealm builds the LDAP search filter from the client username without RFC-4515 escaping (#1348)
    
    (cherry picked from commit 3cb8ef745c44b39377d855d835198de183a4cef3)
---
 .../knox/gateway/shirorealm/KnoxLdapRealm.java     | 48 +++++++++++++--
 .../knox/gateway/shirorealm/KnoxLdapRealmTest.java | 70 ++++++++++++++++++++++
 2 files changed, 114 insertions(+), 4 deletions(-)

diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java
index 8b9f78d1a..2306230ba 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealm.java
@@ -700,10 +700,10 @@ public class KnoxLdapRealm extends DefaultLdapRealm {
               "(&(objectclass=%1$s)(%2$s=%3$s))",
               getUserObjectClass(),
               userSearchAttributeName,
-              expandTemplate( getUserSearchAttributeTemplate(), matchedPrincipal ) );
+              expandTemplate(getUserSearchAttributeTemplate(), matchedPrincipal, true));
         }
       } else {
-        searchFilter = expandTemplate( userSearchFilter, matchedPrincipal );
+        searchFilter = expandTemplate(userSearchFilter, matchedPrincipal, true);
       }
       SearchControls searchControls = getUserSearchControls();
 
@@ -749,17 +749,57 @@ public class KnoxLdapRealm extends DefaultLdapRealm {
       return new SimpleAuthenticationInfo(token.getPrincipal(), credentialsHash.toHex(), credentialsHash.getSalt(), getName());
     }
 
-  private static String expandTemplate( final String template, final Matcher input ) {
+  private static String expandTemplate(final String template, final Matcher input) {
+    return expandTemplate(template, input, false);
+  }
+
+  private static String expandTemplate( final String template, final Matcher input, final boolean escapeForLdapFilter ) {
     String output = template;
     Matcher matcher = TEMPLATE_PATTERN.matcher( output );
     while( matcher.find() ) {
       String lookupStr = matcher.group( 1 );
       int lookupIndex = Integer.parseInt( lookupStr );
       String lookupValue = input.group( lookupIndex );
-      output = matcher.replaceFirst( lookupValue == null ? "" : lookupValue );
+      if (lookupValue == null) {
+          lookupValue = "";
+      } else if (escapeForLdapFilter) {
+          lookupValue = escapeLdapSearchFilterValue(lookupValue);
+      }
+      // quoteReplacement is required: replaceFirst treats '\' and '$' in the replacement specially
+      output = matcher.replaceFirst(Matcher.quoteReplacement(lookupValue));
       matcher = TEMPLATE_PATTERN.matcher( output );
     }
     return output;
   }
 
+  private static String escapeLdapSearchFilterValue(final String value) {
+    if (value == null) {
+      return null;
+    }
+    final StringBuilder sb = new StringBuilder(value.length());
+    for (int i = 0; i < value.length(); i++) {
+      final char c = value.charAt(i);
+      switch (c) {
+        case '\\':
+          sb.append("\\5c");
+          break;
+        case '*':
+          sb.append("\\2a");
+          break;
+        case '(':
+          sb.append("\\28");
+          break;
+        case ')':
+          sb.append("\\29");
+          break;
+        case '\0':
+          sb.append("\\00");
+          break;
+        default:
+          sb.append(c);
+      }
+    }
+    return sb.toString();
+  }
+
 }
diff --git a/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealmTest.java b/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealmTest.java
index d26bdbbe0..63261602f 100644
--- a/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealmTest.java
+++ b/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapRealmTest.java
@@ -19,13 +19,83 @@
 
 package org.apache.knox.gateway.shirorealm;
 
+import org.apache.shiro.realm.ldap.LdapContextFactory;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
 import org.junit.Test;
 
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapContext;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
 public class KnoxLdapRealmTest {
 
+  private static String captureSearchFilter(KnoxLdapRealm realm, String principal) throws Exception {
+    LdapContextFactory factory = EasyMock.createNiceMock(LdapContextFactory.class);
+    LdapContext ctx = EasyMock.createNiceMock(LdapContext.class);
+    NamingEnumeration<SearchResult> results = EasyMock.createNiceMock(NamingEnumeration.class);
+
+    EasyMock.expect(factory.getSystemLdapContext()).andReturn(ctx).anyTimes();
+    Capture<String> filter = EasyMock.newCapture();
+    EasyMock.expect(ctx.search(EasyMock.anyString(), EasyMock.capture(filter),
+        EasyMock.anyObject(SearchControls.class))).andReturn(results);
+    EasyMock.expect(results.hasMore()).andReturn(false).anyTimes();
+    EasyMock.replay(factory, ctx, results);
+
+    realm.setContextFactory(factory);
+    try {
+      realm.getUserDn(principal);
+    } catch (IllegalArgumentException expected) {
+      // mock returns no entry, so getUserDn throws after the search; we only need the filter
+    }
+    return filter.getValue();
+  }
+
+  private static KnoxLdapRealm searchModeRealm() {
+    KnoxLdapRealm realm = new KnoxLdapRealm();
+    realm.setSearchBase("dc=hadoop,dc=apache,dc=org");
+    realm.setUserSearchBase("ou=people,dc=hadoop,dc=apache,dc=org");
+    realm.setUserSearchAttributeName("uid");
+    realm.setUserObjectClass("person");
+    return realm;
+  }
+
+  @Test
+  public void getUserDnEscapesLdapFilterMetacharacters() throws Exception {
+    String filter = captureSearchFilter(searchModeRealm(), "*)(uid=admin");
+    assertEquals("(&(objectclass=person)(uid=\\2a\\29\\28uid=admin))", filter);
+  }
+
+  @Test
+  public void getUserDnEscapesWildcard() throws Exception {
+    String filter = captureSearchFilter(searchModeRealm(), "*");
+    assertEquals("(&(objectclass=person)(uid=\\2a))", filter);
+  }
+
+  @Test
+  public void getUserDnEscapesBackslash() throws Exception {
+    String filter = captureSearchFilter(searchModeRealm(), "a\\b");
+    assertEquals("(&(objectclass=person)(uid=a\\5cb))", filter);
+  }
+
+  @Test
+  public void getUserDnLeavesLegitimateUsernameUnchanged() throws Exception {
+    String filter = captureSearchFilter(searchModeRealm(), "sam");
+    assertEquals("(&(objectclass=person)(uid=sam))", filter);
+  }
+
+  @Test
+  public void getUserDnEscapesValueButPreservesOperatorFilterStructure() throws Exception {
+    KnoxLdapRealm realm = searchModeRealm();
+    realm.setUserSearchFilter("(uid={0})");
+    String filter = captureSearchFilter(realm, "a)(b");
+    assertEquals("(uid=a\\29\\28b)", filter);
+  }
+
   @Test
   public void setGetSearchBase() {
     KnoxLdapRealm realm = new KnoxLdapRealm();