java-topology/test/jdk/com/sun/jndi/ldap/LdapClientConnTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

413 lines
18 KiB
Java

/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import javax.naming.Context;
import javax.naming.InitialContext;
import jdk.test.lib.net.URIBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.junit.jupiter.api.Assertions.fail;
/*
* @test
* @bug 8357708
* @summary verify that com.sun.jndi.ldap.Connection does not ignore the LDAP replies
* that were received before the Connection was closed.
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder
* @run junit/othervm LdapClientConnTest
*/
public class LdapClientConnTest {
private static final byte BER_TYPE_LDAP_SEQUENCE = 0x30;
private static final byte BER_TYPE_INTEGER = 0x02;
private static final byte BER_TYPE_OCTET_STRING = 0x04;
private static final byte BER_TYPE_ENUM = 0x0a;
private static final byte BER_TYPE_LDAP_SEARCH_REQUEST_OP = 0x63;
private static final byte BER_TYPE_LDAP_SEARCH_RESULT_ENTRY_OP = 0x64;
private static final byte BER_TYPE_LDAP_SEARCH_RESULT_DONE_OP = 0x65;
private static final byte BER_TYPE_LDAP_SEARCH_RESULT_REFERENCE_OP = 0x73;
private static final byte LDAP_SEARCH_RESULT_DONE_SUCCESS = 0x00;
private static final String SEARCH_REQ_DN_PREFIX = "CN=foo-";
private static final String SEARCH_REQ_DN_SUFFIX = "-bar";
private static Server server;
private static final List<Throwable> serverSideFailures =
Collections.synchronizedList(new ArrayList<>());
@BeforeAll
static void beforeAll() throws Exception {
server = startServer();
System.out.println("server started " + server.getAddress());
}
@AfterAll
static void afterAll() {
if (server != null) {
System.out.println("stopping server " + server.getAddress());
server.close();
}
}
/*
* Launches several concurrent tasks, all of which use a LdapClient of their own to trigger
* a LDAP SEARCH request. The server side handles the LDAP SEARCH request and writes out the
* response over the Socket and then after the response is written out, closes the
* OutputStream of the Socket. The test then verifies that each of these tasks complete
* normally without any exception being raised.
*/
@Test
public void testLdapRepliesNotIgnored() throws Throwable {
final Map<String, Future<Void>> results = new HashMap<>();
final int numTasks = 10;
try (final ExecutorService executor = Executors.newCachedThreadPool()) {
for (int i = 1; i <= numTasks; i++) {
final String taskName = "task-" + i;
results.put(taskName, executor.submit(new LdapRequestsTask(taskName)));
}
System.out.println("waiting for " + numTasks + " to complete");
for (final Map.Entry<String, Future<Void>> entry : results.entrySet()) {
try {
entry.getValue().get();
} catch (ExecutionException ee) {
final Throwable cause = ee.getCause();
System.out.println("failed for " + entry.getKey() + ", exception: " + cause);
throw cause;
}
}
}
// verify there weren't any server side failures
if (!serverSideFailures.isEmpty()) {
System.err.println("server side failure(s) follow:");
for (final Throwable t : serverSideFailures) {
t.printStackTrace();
}
fail("unexpected server side failures");
}
}
private static Server startServer() throws IOException {
final ServerSocket serverSocket = new ServerSocket(0, 0, InetAddress.getLoopbackAddress());
final Server s = new Server(serverSocket);
s.start();
return s;
}
// accepts connections on a ServerSocket and hands off the request processing
// to the RequestHandler
private static final class Server implements Runnable, AutoCloseable {
private final ServerSocket serverSocket;
private final AtomicInteger reqHandlerTid = new AtomicInteger();
private volatile boolean stop;
private Server(final ServerSocket serverSocket) {
this.serverSocket = serverSocket;
}
@Override
public void run() {
System.err.println("Server accepting connections at "
+ serverSocket.getLocalSocketAddress());
while (!stop) {
try {
final Socket accepted = serverSocket.accept();
System.err.println("Accepted connection from " + accepted);
dispatchRequest(accepted);
} catch (Throwable t) {
if (!stop) {
System.err.println("Server thread ran into unexpected exception: " + t);
t.printStackTrace();
// keep track of this failure to eventually fail the test
serverSideFailures.add(t);
return;
}
}
}
}
private void start() {
final Thread serverThread = new Thread(this);
serverThread.setName("server");
serverThread.setDaemon(true);
serverThread.start();
}
private InetSocketAddress getAddress() {
return (InetSocketAddress) this.serverSocket.getLocalSocketAddress();
}
private void dispatchRequest(final Socket incomingConnection) {
final RequestHandler handler = new RequestHandler(incomingConnection);
// handle the request in a separate thread
final Thread reqHandlerThread = new Thread(handler);
reqHandlerThread.setName("request-handler-" + reqHandlerTid.incrementAndGet());
reqHandlerThread.setDaemon(true);
reqHandlerThread.start();
}
@Override
public void close() {
this.stop = true;
try {
System.err.println("closing server socket " + this.serverSocket);
this.serverSocket.close();
} catch (IOException _) {
// ignore
}
}
}
// Handles a single request over the Socket and responds back on the same Socket
private static final class RequestHandler implements Runnable {
private record SearchRequest(byte msgId, String dn) {
}
private final Socket clientSocket;
private RequestHandler(final Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
final String threadName = Thread.currentThread().getName();
System.err.println(threadName + " - handling request on socket: " + clientSocket);
try (InputStream is = clientSocket.getInputStream();
OutputStream os = clientSocket.getOutputStream()) {
final SearchRequest searchRequest = parseLDAPSearchRequest(is);
// generate a LDAP response
final byte[] responseBytes = makeResponse(searchRequest.msgId,
searchRequest.dn, server.getAddress());
System.err.println(threadName + " - responding to: " + searchRequest);
os.write(responseBytes);
} catch (Throwable t) {
System.err.println(threadName + " - exception in request handler: " + t);
t.printStackTrace();
// keep track of this failure to eventually fail the test
serverSideFailures.add(t);
} finally {
System.err.println(threadName + " - request handler done");
}
}
private static SearchRequest parseLDAPSearchRequest(final InputStream is)
throws IOException {
final String threadName = Thread.currentThread().getName();
final HexFormat hf = HexFormat.of();
// read the BER elements
// each BER element is 3 parts:
// Type, length, value
final int berType = is.read();
if (berType != BER_TYPE_LDAP_SEQUENCE) {
// unexpected content
throw new IOException(threadName + " - unexpected request, not a LDAP_SEQUENCE: "
+ hf.formatHex(new byte[]{(byte) berType}));
}
// BER element length
int seqLen = is.read();
// 0x81, 0x82, 0x84 (and a few others) represent length that is represented
// in multiple bytes. for this test we only consider length represented in
// single byte or multiple bytes through 0x81 and 0x82
if (seqLen == 0x81) {
seqLen = is.read() & 0xff;
} else if (seqLen == 0x82) {
seqLen = (is.read() & 0xff) << 8 + (is.read() & 0xff);
}
if (seqLen < 0) {
// unexpected BER element length
throw new IOException(threadName + " - unexpected BER element length: " + seqLen);
}
// read the BER element value
final byte[] ldapSeq = new byte[seqLen];
System.err.println(threadName + " - reading " + seqLen + " bytes from request");
is.readNBytes(ldapSeq, 0, seqLen);
final String ldapSeqHex = HexFormat.of().formatHex(ldapSeq); // just for debug logging
System.err.println(threadName + " - request LDAP sequence: 0x" + ldapSeqHex);
// read the message id BER element from the LDAP sequence
final byte msgIdType = ldapSeq[0];
if (msgIdType != BER_TYPE_INTEGER) {
// unexpected content
throw new IOException(threadName + " - unexpected BER type for message id element: "
+ hf.formatHex(new byte[]{msgIdType}));
}
final byte msgIdLen = ldapSeq[1];
final byte msgId = ldapSeq[2];
// read LDAP operation type
final byte ldapOpType = ldapSeq[3];
if (ldapOpType != BER_TYPE_LDAP_SEARCH_REQUEST_OP) {
// we only support LDAP search requests in this handler
throw new IOException(threadName + " - unexpected BER type for LDAP operation: "
+ hf.formatHex(new byte[]{ldapOpType}));
}
final byte searchReqSeqLen = ldapSeq[4];
if (searchReqSeqLen < 0) {
// implies the length is represented in multiple bytes. we don't
// expect that big a search request payload in this test, so fail.
throw new IOException(threadName + " - unexpected length for SEARCH request: "
+ hf.formatHex(new byte[]{searchReqSeqLen}));
}
// not all characters will be ASCII, but that's OK, this is here merely as a check
// for unexpected requests
final String remainingPayload = new String(ldapSeq, 5, (ldapSeq.length - 5));
final int dnPrefixIndex = remainingPayload.indexOf(SEARCH_REQ_DN_PREFIX);
final int dnSuffixIndex = remainingPayload.indexOf(SEARCH_REQ_DN_SUFFIX);
if (dnPrefixIndex < 0 || dnSuffixIndex < 0) {
throw new IOException(threadName + " - missing expected DN in SEARCH request: "
+ remainingPayload);
}
final String dn = remainingPayload.substring(dnPrefixIndex,
dnSuffixIndex + SEARCH_REQ_DN_SUFFIX.length());
return new SearchRequest(msgId, dn);
}
// constructs and returns a byte[] response containing the following (in that order):
// - Search Result Reference
// - Search Result Entry
// - Search Result Done
private static byte[] makeResponse(final byte msgId, final String origDN,
final InetSocketAddress targetServer)
throws IOException {
// construct a URI with a different DN for using as referral URI
final String newPrefix = SEARCH_REQ_DN_PREFIX + "dummy-referral-";
final String newDN = origDN.replace(SEARCH_REQ_DN_PREFIX, newPrefix);
final String referralURI = URIBuilder.newBuilder()
.scheme("ldap")
.host(targetServer.getAddress().getHostAddress())
.port(targetServer.getPort())
.path("/" + newDN)
.buildUnchecked()
.toString();
final byte msgIdLen = 1;
final ByteArrayOutputStream bout = new ByteArrayOutputStream();
// write the BER elements
// each BER element is 3 parts:
// Type, length, value
// Search Result Reference BER element (refer to LDAPv3 wire format for details)
bout.write(BER_TYPE_LDAP_SEQUENCE);
bout.write(referralURI.length() + 7);
bout.write(new byte[]{BER_TYPE_INTEGER, msgIdLen, msgId});
bout.write(BER_TYPE_LDAP_SEARCH_RESULT_REFERENCE_OP);
bout.write(referralURI.length() + 2);
bout.write(BER_TYPE_OCTET_STRING);
bout.write(referralURI.length());
bout.write(referralURI.getBytes(US_ASCII));
// Search Result Entry BER element (refer to LDAPv3 wire format for details)
bout.write(BER_TYPE_LDAP_SEQUENCE);
bout.write(origDN.length() + 9);
bout.write(new byte[]{BER_TYPE_INTEGER, msgIdLen, msgId});
bout.write(BER_TYPE_LDAP_SEARCH_RESULT_ENTRY_OP);
bout.write(origDN.length() + 2);
bout.write(BER_TYPE_OCTET_STRING);
bout.write(origDN.length());
bout.write(origDN.getBytes(US_ASCII));
bout.write(BER_TYPE_LDAP_SEQUENCE);
// 0 length for the LDAP sequence, implying no attributes in this Search Result Entry
bout.write(0);
// Search Result Done BER element (refer to LDAPv3 wire format for details)
bout.write(BER_TYPE_LDAP_SEQUENCE);
bout.write(origDN.length() + 12);
bout.write(new byte[]{BER_TYPE_INTEGER, msgIdLen, msgId});
bout.write(BER_TYPE_LDAP_SEARCH_RESULT_DONE_OP);
bout.write(7);
bout.write(new byte[]{BER_TYPE_ENUM, 1, LDAP_SEARCH_RESULT_DONE_SUCCESS});
// the matched DN
bout.write(BER_TYPE_OCTET_STRING);
bout.write(origDN.length());
bout.write(origDN.getBytes(US_ASCII));
// 0 length implies no diagnostic message
bout.write(new byte[]{BER_TYPE_OCTET_STRING, 0});
return bout.toByteArray();
}
}
// a task that triggers LDAP SEARCH request
private static final class LdapRequestsTask implements Callable<Void> {
private final String taskName;
private LdapRequestsTask(final String taskName) {
this.taskName = taskName;
}
@Override
public Void call() throws Exception {
Context ldapCtx = null;
try {
final InetSocketAddress serverAddr = server.getAddress();
final Hashtable<String, String> envProps = new Hashtable<>();
envProps.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
final String providerUrl = URIBuilder.newBuilder()
.scheme("ldap")
.host(serverAddr.getAddress())
.port(serverAddr.getPort())
.build().toString();
envProps.put(Context.PROVIDER_URL, providerUrl);
// explicitly set LDAP version to 3 to prevent LDAP BIND requests
// during LdapCtx instantiation
envProps.put("java.naming.ldap.version", "3");
ldapCtx = new InitialContext(envProps);
final String name = SEARCH_REQ_DN_PREFIX + taskName + SEARCH_REQ_DN_SUFFIX;
// trigger the LDAP SEARCH requests through the lookup call. we are not
// interested in the returned value and are merely interested in a normal
// completion of the call.
final var _ = ldapCtx.lookup(name);
return null;
} finally {
if (ldapCtx != null) {
ldapCtx.close();
}
}
}
}
}