This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 6a5f5362c8d8ae8bf44472f610166e5e0ebab877 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 a8bb7256b5..561ed09a69 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 84e84b3bda..2d5e2ecf6a 100644 --- a/java/org/apache/catalina/realm/LocalStrings.properties +++ b/java/org/apache/catalina/realm/LocalStrings.properties @@ -98,6 +98,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]
