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

tomaswolf pushed a commit to branch rc-2.20.0
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit acc0f1ed436529c24f6677da83e7ec4e3537c4c9
Author: Thomas Wolf <[email protected]>
AuthorDate: Sat Sep 19 23:09:00 2026 +0200

    Fix LDAP authentication
    
    Properly encode filter parameters following RFC 4515, and encode
    parameters for the distinguished name following RFC 4514. Also harden
    the password authenticator by requiring a non-empty result from LDAP.
---
 .../org/apache/sshd/ldap/LdapNetworkConnector.java | 75 +++++++++++++++++++++-
 .../sshd/ldap/LdapPasswordAuthenticator.java       |  6 +-
 .../org/apache/sshd/ldap/LdapEncodingTest.java     | 47 ++++++++++++++
 .../sshd/ldap/LdapPasswordAuthenticatorTest.java   | 10 ++-
 .../sshd/ldap/LdapPublickeyAuthenticatorTest.java  | 14 ++--
 5 files changed, 137 insertions(+), 15 deletions(-)

diff --git 
a/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapNetworkConnector.java 
b/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapNetworkConnector.java
index 5be56e073..d6ce5c522 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapNetworkConnector.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapNetworkConnector.java
@@ -19,6 +19,7 @@
 
 package org.apache.sshd.ldap;
 
+import java.nio.charset.StandardCharsets;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -88,6 +89,9 @@ public class LdapNetworkConnector<C> extends NetworkConnector 
{
               + 
",userPassword,userCertificate,cACertificate,authorityRevocationList,certificateRevocationList"
               + ",crossCertificatePair,x500UniqueIdentifier";
 
+    private static final char[] HEX = new char[] {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 
'D', 'E', 'F' };
+
     protected final SearchControls searchControls = new SearchControls();
     protected final Map<String, Object> ldapEnv = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
     protected MessageFormat bindDNPattern = new 
MessageFormat(DEFAULT_LDAP_BIND_DN_PATTERN);
@@ -435,13 +439,14 @@ public class LdapNetworkConnector<C> extends 
NetworkConnector {
         String mode = 
Objects.toString(env.get(Context.SECURITY_AUTHENTICATION), null);
         boolean anonymous = GenericUtils.isEmpty(mode) || 
PropertyResolverUtils.isNoneValue(mode);
         if (!anonymous) {
-            Object[] bindParams = { username, password };
             if (!env.containsKey(Context.SECURITY_PRINCIPAL)) {
+                Object[] bindParams = { encodeDistinguishedName(username), 
encodeDistinguishedName(password) };
                 String bindDN = Objects.requireNonNull(bindDNPattern, "No bind 
DN pattern").format(bindParams);
                 env.put(Context.SECURITY_PRINCIPAL, 
ValidateUtils.checkNotNullAndNotEmpty(bindDN, "No bind DN"));
             }
 
             if (!env.containsKey(Context.SECURITY_CREDENTIALS)) {
+                Object[] bindParams = { username, password };
                 String bindPassword
                         = Objects.requireNonNull(bindPasswordPattern, "No bind 
password pattern").format(bindParams);
                 env.put(Context.SECURITY_CREDENTIALS, 
ValidateUtils.checkNotNullAndNotEmpty(bindPassword, "No bind password"));
@@ -453,13 +458,13 @@ public class LdapNetworkConnector<C> extends 
NetworkConnector {
 
     protected String resolveBaseDN(C queryContext, Map<?, ?> ldapConfig, 
String username, String password)
             throws NamingException {
-        Object[] bindParams = { username, password };
+        Object[] bindParams = { encodeDistinguishedName(username), 
encodeDistinguishedName(password) };
         return Objects.requireNonNull(baseDNPattern, "No base DN 
pattern").format(bindParams);
     }
 
     protected String resolveSearchFilter(C queryContext, Map<?, ?> ldapConfig, 
String username, String password)
             throws NamingException {
-        Object[] bindParams = { username, password };
+        Object[] bindParams = { encodeFilter(username), encodeFilter(password) 
};
         return Objects.requireNonNull(searchFilterPattern, "No search filter 
pattern").format(bindParams);
     }
 
@@ -552,4 +557,68 @@ public class LdapNetworkConnector<C> extends 
NetworkConnector {
 
         return attrVal.toString();
     }
+
+    /**
+     * Encodes a value following RFC 4515 for use in an LDAP filter template.
+     *
+     * @param  value to encode
+     * @return       the encoded value
+     * @see          <a href="https://www.rfc-editor.org/info/rfc4515/";>RFC 
4515</a>
+     */
+    public static String encodeFilter(String value) {
+        if (value == null) {
+            return "";
+        }
+        byte[] utf8 = value.getBytes(StandardCharsets.UTF_8);
+        StringBuilder sb = new StringBuilder(utf8.length);
+        for (byte b : utf8) {
+            int ch = b & 0xFF;
+            if (ch < ' ' || ch == '*' || ch == '(' || ch == ')' || ch == '\\' 
|| ch >= 0x7F) {
+                sb.append('\\').append(HEX[ch >>> 4]).append(HEX[ch & 0x0F]);
+            } else {
+                sb.append((char) ch);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Encodes a value following RFC 4514 for use in an LDAP Distinguished 
Name.
+     *
+     * @param  value to encode
+     * @return       the encoded value
+     * @see          <a href="https://www.rfc-editor.org/info/rfc4514/";>RFC 
4514</a>
+     */
+    public static String encodeDistinguishedName(String value) {
+        if (value == null) {
+            return "";
+        }
+        // Note: javax.naming.ldap.Rdn.escapeValue() does not escape NUL nor 
other control characters. It escapes _all_
+        // leading and trailing spaces (and \r), and _all_ hashes. It 
implements RFC 2553, which didn't mention NUL and
+        // which was obsoleted by RFC 4514.
+        byte[] utf8 = value.getBytes(StandardCharsets.UTF_8);
+        StringBuilder sb = new StringBuilder(utf8.length);
+        for (int i = 0; i < utf8.length; i++) {
+            int ch = utf8[i] & 0xFF;
+            if (ch == ' ') {
+                if (i == 0 || i == utf8.length - 1) {
+                    sb.append('\\');
+                }
+                sb.append((char) ch);
+            } else if (ch == '#') {
+                if (i == 0) {
+                    sb.append('\\');
+                }
+                sb.append((char) ch);
+            } else if (ch == '"' || ch == '+' || ch == ',' || ch == ';' || ch 
== '<' || ch == '>' || ch == '\\') {
+                sb.append('\\').append((char) ch);
+            } else if (ch < ' ' || ch >= 0x7F) {
+                sb.append('\\').append(HEX[ch >>> 4]).append(HEX[ch & 0x0F]);
+            } else {
+                sb.append((char) ch);
+            }
+        }
+        return sb.toString();
+    }
+
 }
diff --git 
a/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapPasswordAuthenticator.java 
b/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapPasswordAuthenticator.java
index 3b3945ca2..bb7ba36bb 100644
--- 
a/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapPasswordAuthenticator.java
+++ 
b/sshd-ldap/src/main/java/org/apache/sshd/ldap/LdapPasswordAuthenticator.java
@@ -75,10 +75,6 @@ public class LdapPasswordAuthenticator extends 
LdapAuthenticator implements Pass
 
     protected boolean authenticate(
             String username, String password, ServerSession session, 
Map<String, ?> attrs) {
-        /*
-         * By default we assume that the user + password are the same for 
accessing the LDAP as the user's account, so
-         * the very LDAP query success is enough
-         */
-        return true;
+        return attrs != null && !attrs.isEmpty();
     }
 }
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapEncodingTest.java 
b/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapEncodingTest.java
new file mode 100644
index 000000000..afff7e964
--- /dev/null
+++ b/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapEncodingTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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
+ *
+ * http://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.sshd.ldap;
+
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag("NoIoTestCase")
+public class LdapEncodingTest extends JUnitTestSupport {
+
+    @Test
+    void testEncodeFilter() {
+        assertEquals("\\00\\7F\\C3\\BF\\2A\\28\\29\\5Ca0B\\C3\\A4\\C3\\A9",
+                LdapNetworkConnector.encodeFilter("\000\177\377*()\\a0Bäé"));
+        assertEquals("Lu\\C4\\8Di\\C4\\87", 
LdapNetworkConnector.encodeFilter("Lučić")); // From RFC 4515
+    }
+
+    @Test
+    void testEncodeDN() {
+        assertEquals("J. Smith", 
LdapNetworkConnector.encodeDistinguishedName("J. Smith"));
+        assertEquals("James \\\"Jim\\\" Smith", 
LdapNetworkConnector.encodeDistinguishedName("James \"Jim\" Smith"));
+        assertEquals("Before\\0DAfter", 
LdapNetworkConnector.encodeDistinguishedName("Before\rAfter"));
+        assertEquals("Lu\\C4\\8Di\\C4\\87", 
LdapNetworkConnector.encodeDistinguishedName("Lučić"));
+        assertEquals("J\\C3\\B6rg M\\C3\\BCller", 
LdapNetworkConnector.encodeDistinguishedName("Jörg Müller"));
+        assertEquals("\\#foo", 
LdapNetworkConnector.encodeDistinguishedName("#foo"));
+        assertEquals("\\  foo \\ ", 
LdapNetworkConnector.encodeDistinguishedName("  foo  "));
+        assertEquals("\\  foo\\00\\+\\,\\;\\<\\>\\\\bar\\ ",
+                LdapNetworkConnector.encodeDistinguishedName("  
foo\000+,;<>\\bar "));
+    }
+}
diff --git 
a/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPasswordAuthenticatorTest.java
 
b/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPasswordAuthenticatorTest.java
index 37ea3cbad..7414bc3a7 100644
--- 
a/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPasswordAuthenticatorTest.java
+++ 
b/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPasswordAuthenticatorTest.java
@@ -35,9 +35,6 @@ import org.junit.jupiter.api.TestMethodOrder;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mockito;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
 /**
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
@@ -73,6 +70,13 @@ public class LdapPasswordAuthenticatorTest extends 
BaseAuthenticatorTest {
         usersMap.forEach((username, password) -> {
             outputDebugMessage("Authenticate: user=%s, password=%s", username, 
password);
             assertTrue(auth.authenticate(username, password, session), "Failed 
to authenticate " + username);
+            String otherPassword = password + "bar";
+            outputDebugMessage("Authenticate: user=%s, password=%s", username, 
otherPassword);
+            assertFalse(auth.authenticate(username, otherPassword, session), 
"Should not have authenticated " + username);
         });
+        String otherUser = "other";
+        assertFalse(usersMap.containsKey(otherUser));
+        assertFalse(auth.authenticate(otherUser, otherUser, session), "Should 
not have authenticated other");
+        assertFalse(auth.authenticate("*", "*", session), "Should not have 
authenticated *");
     }
 }
diff --git 
a/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPublickeyAuthenticatorTest.java
 
b/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPublickeyAuthenticatorTest.java
index 827e2692f..07c788158 100644
--- 
a/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPublickeyAuthenticatorTest.java
+++ 
b/sshd-ldap/src/test/java/org/apache/sshd/ldap/LdapPublickeyAuthenticatorTest.java
@@ -19,6 +19,8 @@
 
 package org.apache.sshd.ldap;
 
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
 import java.security.PublicKey;
 import java.util.Collections;
 import java.util.Comparator;
@@ -43,9 +45,6 @@ import org.junit.jupiter.api.TestMethodOrder;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mockito;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
 /**
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
@@ -88,12 +87,19 @@ public class LdapPublickeyAuthenticatorTest extends 
BaseAuthenticatorTest {
         auth.setKeyAttributeName(TEST_ATTR_NAME);
         auth.setRetrievedAttributes(TEST_ATTR_NAME);
 
+        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
+        gen.initialize(1024);
+        KeyPair bogus = gen.generateKeyPair();
         ServerSession session = Mockito.mock(ServerSession.class);
         outputDebugMessage("%s: %s", getCurrentTestName(), auth);
         KEYS_MAP.forEach((username, key) -> {
             outputDebugMessage("Authenticate: user=%s, key-type=%s, 
fingerprint=%s",
                     username, KeyUtils.getKeyType(key), 
KeyUtils.getFingerPrint(key));
-            assertTrue(auth.authenticate(username, key, session), "Failed to 
authenticate user=" + username);
+            assertTrue(auth.authenticate(username, key, session), "Failed to 
authenticate user = " + username);
+            assertFalse(auth.authenticate(username, bogus.getPublic(), 
session),
+                    "Should not have authenticated user = " + username);
+            assertFalse(auth.authenticate("other", key, session), "Should not 
have authenticated user = other");
+            assertFalse(auth.authenticate("*", key, session), "Should not have 
authenticated user = *");
         });
     }
 }

Reply via email to