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

rmaucher pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 55b5db0fc9526c0ac8b279feca1199d06a57b02a
Author: opencode <[email protected]>
AuthorDate: Wed Oct 7 16:15:23 2026 +0200

    Report missing user credentials as an authentication failure in JNDIRealm
    
    userCredentialsAdd() put the user credentials directly into the JNDI
    environment, which is a Hashtable that rejects null values. When an
    as-user search was reached without credentials, for example with
    userSearchAsUser or roleSearchAsUser enabled and a GSS or client
    certificate principal lookup, the realm failed with a raw
    NullPointerException instead of a clean authentication failure.
    
    Throw an AuthenticationException with a clear message when no
    credentials are available for the user. The check runs before the
    environment is modified, so pooled connections are never left with
    partial security settings.
---
 java/org/apache/catalina/realm/JNDIRealm.java      |  8 +-
 .../apache/catalina/realm/LocalStrings.properties  |  1 +
 .../catalina/realm/TestJNDIRealmSearchAsUser.java  | 99 ++++++++++++++++++++++
 3 files changed, 107 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/catalina/realm/JNDIRealm.java 
b/java/org/apache/catalina/realm/JNDIRealm.java
index 22fcca508b..a2fdbe436d 100644
--- a/java/org/apache/catalina/realm/JNDIRealm.java
+++ b/java/org/apache/catalina/realm/JNDIRealm.java
@@ -2001,9 +2001,15 @@ public class JNDIRealm extends RealmBase {
      * @param dn          Distinguished name of user
      * @param credentials Credentials of user
      *
-     * @exception NamingException if a directory server error occurs
+     * @exception NamingException if a directory server error occurs or no 
credentials are available for the user
      */
     private void userCredentialsAdd(DirContext context, String dn, String 
credentials) throws NamingException {
+        if (credentials == null) {
+            // The JNDI environment is a Hashtable that does not accept null
+            // values. Without credentials, searching or binding as the user is
+            // not possible.
+            throw new 
AuthenticationException(sm.getString("jndiRealm.noUserCredentials", dn));
+        }
         // Set up security environment to bind as the user
         context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
         context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
diff --git a/java/org/apache/catalina/realm/LocalStrings.properties 
b/java/org/apache/catalina/realm/LocalStrings.properties
index b85215b0e4..e80fb53823 100644
--- a/java/org/apache/catalina/realm/LocalStrings.properties
+++ b/java/org/apache/catalina/realm/LocalStrings.properties
@@ -91,6 +91,7 @@ jndiRealm.invalidSslSocketFactory=[{0}] not a valid class 
name for an SSLSocketF
 jndiRealm.invalidUserPattern=The user pattern [{0}] is not valid: it contains 
unbalanced parentheses
 jndiRealm.multipleEntries=User name [{0}] has multiple entries
 jndiRealm.negotiatedTls=Negotiated tls connection using protocol [{0}]
+jndiRealm.noUserCredentials=Cannot perform the search or bind as user [{0}]: 
no password is available for the user
 jndiRealm.open=Exception opening directory server connection
 jndiRealm.tlsClose=Exception closing tls response
 
diff --git a/test/org/apache/catalina/realm/TestJNDIRealmSearchAsUser.java 
b/test/org/apache/catalina/realm/TestJNDIRealmSearchAsUser.java
new file mode 100644
index 0000000000..e691423d35
--- /dev/null
+++ b/test/org/apache/catalina/realm/TestJNDIRealmSearchAsUser.java
@@ -0,0 +1,99 @@
+/*
+ * 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.catalina.realm;
+
+import java.net.InetAddress;
+
+import javax.naming.AuthenticationException;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.juli.logging.LogFactory;
+
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.sdk.AddRequest;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPResult;
+import com.unboundid.ldap.sdk.ResultCode;
+
+public class TestJNDIRealmSearchAsUser {
+
+    private static InMemoryDirectoryServer ldapServer;
+
+    @BeforeClass
+    public static void createLDAP() throws Exception {
+        InMemoryDirectoryServerConfig config = new 
InMemoryDirectoryServerConfig("dc=example,dc=com");
+        InetAddress localhost = InetAddress.getByName("localhost");
+        InMemoryListenerConfig listenerConfig =
+                new InMemoryListenerConfig("localListener", localhost, 0, 
null, null, null);
+        config.setListenerConfigs(listenerConfig);
+        ldapServer = new InMemoryDirectoryServer(config);
+
+        ldapServer.startListening();
+
+        try (LDAPConnection conn = ldapServer.getConnection()) {
+            AddRequest addBase = new AddRequest(
+                    "dn: dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: domain",
+                    "dc: example");
+            LDAPResult result = conn.processOperation(addBase);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+
+            AddRequest addUserTest = new AddRequest(
+                    "dn: cn=test,dc=example,dc=com",
+                    "objectClass: top",
+                    "objectClass: person",
+                    "objectClass: organizationalPerson",
+                    "cn: test",
+                    "sn: Test");
+            result = conn.processOperation(addUserTest);
+            Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode());
+        }
+    }
+
+    @AfterClass
+    public static void destroyLDAP() {
+        ldapServer.shutDown(true);
+    }
+
+    @Test
+    public void testUserSearchAsUserWithoutCredentials() throws Exception {
+        JNDIRealm realm = new JNDIRealm();
+        realm.containerLog = 
LogFactory.getLog(TestJNDIRealmSearchAsUser.class);
+
+        realm.setConnectionURL("ldap://localhost:"; + 
ldapServer.getListenPort());
+        realm.setUserSearch("cn={0}");
+        realm.setUserSearchAsUser(true);
+
+        JNDIRealm.JNDIConnection connection = realm.get();
+        try {
+            realm.getUser(connection, "test");
+            Assert.fail("Searching as the user was expected to fail since no " 
+
+                    "password is available for the user");
+        } catch (AuthenticationException e) {
+            // Expected. Before the fix this was a NullPointerException.
+        } finally {
+            realm.release(connection);
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to