From 253821ee00fd197150195d67f13547f44922389b Mon Sep 17 00:00:00 2001 From: Inf1mum Date: Sat, 7 Nov 2026 02:11:21 +0300 Subject: [PATCH 1/3] fix: embed httpclient5 5.6.4 transport instead of docker-java-transport-zerodep docker-java-transport-zerodep:3.7.1 bundles httpclient5 5.5.1 and httpcore5(-h2) 5.3.6 inside its own jar (relocated under com.github.dockerjava.zerodep.shaded.*, with embedded META-INF/maven metadata), which CVE scanners flag as CVE-2026-54428: an HTTP/2 HPACK decoder DoS in HttpComponents Core, affecting 5.4.2 and earlier and fixed in httpcore5 5.4.3. The code is baked into the zerodep jar, so it cannot be excluded or pinned on the testcontainers side, and docker-java has not released a fixed version yet (docker-java/docker-java#2669 is still open). Instead of depending on the pre-shaded zerodep jar, embed docker-java-transport-httpclient5 and httpclient5 5.6.4 (which brings httpcore5 5.4.3, the first fixed release) into the testcontainers jar with the usual org.testcontainers.shaded relocation, switching DockerClientProviderStrategy from ZerodepDockerHttpClient to ApacheDockerHttpClient; their builders are API-compatible and their defaults are identical (maxConnections = Integer.MAX_VALUE, no connection/response timeouts). docker-java-transport, previously a transitive of zerodep, is now declared directly, so core keeps compiling against DockerHttpClient, UnixSocket, NamedPipeSocket and SSLConfig. The httpclient5/httpcore5 resources (version.properties) follow their relocated classes, and the Apache jars' META-INF/DEPENDENCIES/AL2.0/LGPL2.1 entries are dropped, consistent with the existing META-INF/LICENSE/NOTICE exclusions. Fixes #11973 Signed-off-by: Inf1mum --- core/build.gradle | 23 ++++++++++++++++++- .../testcontainers/JarFileShadingTest.java | 4 +++- .../DockerClientProviderStrategy.java | 4 ++-- .../logging_config.md | 2 +- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index d7a54a26afe..3e836c81ed3 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -26,9 +26,21 @@ shadowJar { 'META-INF/proguard/', 'META-INF/versions/*/module-info.class', 'META-INF/services/java.security.Provider', + 'META-INF/DEPENDENCIES', + 'META-INF/AL2.0', + 'META-INF/LGPL2.1', ].each { exclude(it) } } +afterEvaluate { + // gradle/shading.gradle replaces shadowJar.relocators within its own afterEvaluate + // block, so these must be registered afterwards. The auto-generated relocations + // only cover class packages; the httpclient5/httpcore5 resources (version.properties) + // would otherwise stay behind at their original paths. + tasks.shadowJar.relocate('org.apache.hc.client5', 'org.testcontainers.shaded.org.apache.hc.client5') + tasks.shadowJar.relocate('org.apache.hc.core5', 'org.testcontainers.shaded.org.apache.hc.core5') +} + task jarFileTest(type: Test) { useJUnitPlatform() testClassesDirs = sourceSets.jarFileTest.output.classesDirs @@ -84,12 +96,19 @@ dependencies { shaded platform('com.github.docker-java:docker-java-bom:3.7.1') api "com.github.docker-java:docker-java-api" + api 'com.github.docker-java:docker-java-transport' shaded('com.github.docker-java:docker-java-core') { exclude group: 'com.google.guava', module: 'guava' } - api 'com.github.docker-java:docker-java-transport-zerodep' + // Embed the HTTP transport ourselves instead of depending on + // com.github.docker-java:docker-java-transport-zerodep, whose jar bundles + // httpclient5 5.5.1 / httpcore5(-h2) 5.3.6, affected by CVE-2026-54428 + // (see https://github.com/testcontainers/testcontainers-java/issues/11973). + // httpclient5 5.6.4 brings httpcore5 5.4.3, the first fixed version. + shaded 'com.github.docker-java:docker-java-transport-httpclient5' + shaded 'org.apache.httpcomponents.client5:httpclient5:5.6.4' shaded 'com.google.guava:guava:33.3.1-jre' shaded "org.yaml:snakeyaml:2.6" @@ -129,6 +148,8 @@ dependencies { tasks.generatePomFileForMavenJavaPublication.finalizedBy( tasks.register('checkPOMdependencies', org.testcontainers.build.ComparePOMWithLatestReleasedTask) { ignore = [ + // previously pulled in transitively by docker-java-transport-zerodep (#11973) + 'com.github.docker-java:docker-java-transport', ] } ) diff --git a/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java b/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java index 8b6f380f6eb..ce915381f21 100644 --- a/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java +++ b/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java @@ -15,7 +15,9 @@ class JarFileShadingTest extends AbstractJarFileTest { void testPackages() throws Exception { assertThatFileList(root).containsOnly("org", "META-INF"); - assertThatFileList(root.resolve("org")).containsOnly("testcontainers"); + // "publicsuffix" is the data-only resource directory of the shaded httpclient5 + // (effective_tld_names.dat), not relocated, just like in docker-java's zerodep jar + assertThatFileList(root.resolve("org")).containsOnly("testcontainers", "publicsuffix"); } @Test diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java index 7b0aaafc169..2df08ad3d6e 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java @@ -6,11 +6,11 @@ import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientImpl; import com.github.dockerjava.core.RemoteApiVersion; +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; import com.github.dockerjava.transport.NamedPipeSocket; import com.github.dockerjava.transport.SSLConfig; import com.github.dockerjava.transport.UnixSocket; -import com.github.dockerjava.zerodep.ZerodepDockerHttpClient; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; import lombok.Getter; @@ -392,7 +392,7 @@ public static DockerClient getClientForConfig(TransportConfig transportConfig) { switch (transportType) { case "httpclient5": dockerHttpClient = - new ZerodepDockerHttpClient.Builder() + new ApacheDockerHttpClient.Builder() .dockerHost(transportConfig.getDockerHost()) .sslConfig(transportConfig.getSslConfig()) .build(); diff --git a/docs/supported_docker_environment/logging_config.md b/docs/supported_docker_environment/logging_config.md index 6bbe0584434..f9f511cfd6e 100644 --- a/docs/supported_docker_environment/logging_config.md +++ b/docs/supported_docker_environment/logging_config.md @@ -20,7 +20,7 @@ should be included in your classpath to show a reasonable level of log output: - + ``` From b15eede4be1298328adf266e104061fa63a1b593 Mon Sep 17 00:00:00 2001 From: Inf1mum Date: Sat, 7 Nov 2026 10:56:13 +0300 Subject: [PATCH 2/3] fix: keep JNA out of the shaded jar as unrelocated api dependency docker-java-transport-httpclient5 depends on net.java.dev.jna:jna, which previously reached the api configuration transitively via docker-java-transport-zerodep. That kept JNA excluded from the shadow jar and unrelocated. Once zerodep was replaced, JNA resolved inside the shaded configuration and got bundled and relocated to org.testcontainers.shaded.com.sun.jna. Relocated JNA classes cannot bind the native jnidispatch library: its JNI symbols and the embedded resource path stay at com/sun/jna/..., so strategy resolution failed at test runtime with an UnsatisfiedLinkError in RootlessDockerClientProviderStrategy$LibC. and broke ci/circleci: minimal_core. Declare JNA as an api dependency again (5.18.1, the same version the docker-java 3.7.1 transports use), so it is excluded from shading and still provided to consumers through the POM, and add it to the POM comparison ignore list for the same reason as docker-java-transport. Signed-off-by: Inf1mum --- core/build.gradle | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/build.gradle b/core/build.gradle index 3e836c81ed3..a38a37f1987 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -110,6 +110,15 @@ dependencies { shaded 'com.github.docker-java:docker-java-transport-httpclient5' shaded 'org.apache.httpcomponents.client5:httpclient5:5.6.4' + // docker-java-transport-httpclient5 depends on JNA, which used to reach the api + // configuration transitively via docker-java-transport-zerodep. Declared as an api + // dependency here, JNA is excluded from the shaded jar and left unrelocated: shaded + // (relocated) JNA classes cannot bind the native jnidispatch library, whose JNI + // symbols and embedded resource path stay at com/sun/jna/... - relocating them + // breaks DockerClientProviderStrategy resolution with an UnsatisfiedLinkError in + // RootlessDockerClientProviderStrategy (ci/circleci: minimal_core failure). + api 'net.java.dev.jna:jna:5.18.1' + shaded 'com.google.guava:guava:33.3.1-jre' shaded "org.yaml:snakeyaml:2.6" @@ -150,6 +159,8 @@ tasks.generatePomFileForMavenJavaPublication.finalizedBy( ignore = [ // previously pulled in transitively by docker-java-transport-zerodep (#11973) 'com.github.docker-java:docker-java-transport', + // same for JNA, previously pulled in transitively by docker-java-transport-zerodep (#11973) + 'net.java.dev.jna:jna', ] } ) From c24c1e4b0e633a77b3b378f743ca77572711b8b8 Mon Sep 17 00:00:00 2001 From: Inf1mum Date: Sun, 8 Nov 2026 00:49:21 +0300 Subject: [PATCH 3/3] fix: keep shaded httpclient5 wire/header loggers out of org.testcontainers hierarchy The Apache HttpClient wire and header logger names are plain string constants inside the shaded classes, and shadow relocated them along with the code: they became org.testcontainers.shaded.org.apache.hc.client5.http.{wire,headers} and inherited the level of the org.testcontainers logger. Raising that logger to DEBUG (the documented troubleshooting switch) would silently turn on full wire/header logging of the Docker API traffic, including registry credentials (e.g. the Base64-encoded X-Registry-Auth header used for image pulls). Exclude the logger-name string constants from every relocation that matches them: the explicit org.apache.hc.client5 relocator and the auto-generated per-package org.apache.hc.client5.http relocator from gradle/shading.gradle (the first matching relocator wins, so both must skip the names). The shaded httpclient5 now keeps logging to the canonical names org.apache.hc.client5.http.wire / org.apache.hc.client5.http.headers, which do not inherit from org.testcontainers: wire logging stays off by default and only activates when the user explicitly enables that exact logger. The same names cover plain (unshaded) Apache HttpClient 5.x on the classpath. Update the recommended logback configuration (docs and core test resources) to pin both loggers to OFF, document the explicit opt-in with its credential exposure, and note that raising org.testcontainers/com.github.dockerjava to DEBUG does not enable wire logging. Add a jarFileTest regression test scanning the shaded jar's constant pools to fail if the logger names ever get relocated again. See https://github.com/testcontainers/testcontainers-java/issues/11973 Signed-off-by: Inf1mum --- core/build.gradle | 26 +++++++++- .../testcontainers/JarFileShadingTest.java | 49 +++++++++++++++++++ core/src/test/resources/logback-test.xml | 4 ++ .../logging_config.md | 29 ++++++++++- 4 files changed, 105 insertions(+), 3 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index a38a37f1987..501fd89066e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,3 +1,5 @@ +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator + apply plugin: 'com.gradleup.shadow' description = "Testcontainers Core" @@ -37,8 +39,30 @@ afterEvaluate { // block, so these must be registered afterwards. The auto-generated relocations // only cover class packages; the httpclient5/httpcore5 resources (version.properties) // would otherwise stay behind at their original paths. - tasks.shadowJar.relocate('org.apache.hc.client5', 'org.testcontainers.shaded.org.apache.hc.client5') + tasks.shadowJar.relocate('org.apache.hc.client5', 'org.testcontainers.shaded.org.apache.hc.client5') { + exclude 'org.apache.hc.client5.http.wire' + exclude 'org.apache.hc.client5.http.headers' + } tasks.shadowJar.relocate('org.apache.hc.core5', 'org.testcontainers.shaded.org.apache.hc.core5') + + // Keep the Apache HttpClient wire/headers logger names out of the `org.testcontainers` + // hierarchy. The logger names are plain string constants inside the shaded classes + // ("org.apache.hc.client5.http.wire" / "org.apache.hc.client5.http.headers") and shadow + // relocates them along with the code. If they landed under + // `org.testcontainers.shaded...`, they would inherit the level of the `org.testcontainers` + // logger: raising it to DEBUG (the documented troubleshooting switch) would silently turn + // on full wire/header logging of the Docker API traffic, including registry credentials. + // With the original logger names preserved, wire logging stays off by default and only + // activates when the user explicitly enables that exact logger (see + // docs/supported_docker_environment/logging_config.md). The excludes above are not enough + // on their own: the auto-generated per-package relocator for `org.apache.hc.client5.http` + // (gradle/shading.gradle) would still match the strings, so it must skip them as well. + tasks.shadowJar.relocators + .findAll { it instanceof SimpleRelocator && it.pattern == 'org.apache.hc.client5.http' } + .each { relocator -> + relocator.exclude('org.apache.hc.client5.http.wire') + relocator.exclude('org.apache.hc.client5.http.headers') + } } task jarFileTest(type: Test) { diff --git a/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java b/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java index ce915381f21..e6dcbdcac28 100644 --- a/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java +++ b/core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java @@ -4,8 +4,12 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +44,51 @@ void testMetaInfServices() throws Exception { .allMatch(it -> it.startsWith("org.testcontainers.")); } + @Test + void testHttpClientWireLoggersAreNotRelocated() throws Exception { + // The shaded Apache HttpClient must send wire/header output to the plain + // "org.apache.hc.client5.http.wire" / "org.apache.hc.client5.http.headers" loggers. + // The logger names are plain string constants, and if they were relocated along with + // the code (to "org.testcontainers.shaded..."), the loggers would inherit the level of + // the `org.testcontainers` logger: raising it to DEBUG (the documented troubleshooting + // switch in docs/supported_docker_environment/logging_config.md) would then print the + // full Docker API traffic, including registry credentials. Wire logging must stay + // opt-in on the exact, unrelocated logger names. + Path shadedHttpClientClasses = root + .resolve("org/testcontainers/shaded/org/apache/hc") + .toAbsolutePath() + .normalize(); + + try (Stream entries = Files.walk(shadedHttpClientClasses)) { + List shadedClasses = entries + .filter(path -> path.toString().endsWith(".class")) + .collect(Collectors.toList()); + + assertThat(shadedClasses).isNotEmpty(); + + for (Path shadedClass : shadedClasses) { + String constantPool = new String(Files.readAllBytes(shadedClass), StandardCharsets.ISO_8859_1); + assertThat(constantPool) + .as("constant pool of %s", shadedClass) + .doesNotContain("org.testcontainers.shaded.org.apache.hc.client5.http.wire") + .doesNotContain("org.testcontainers.shaded.org.apache.hc.client5.http.headers"); + } + } + + // the wire/header logger names must keep their original, opt-in values + String connectionClass = new String( + Files.readAllBytes( + root.resolve( + "org/testcontainers/shaded/org/apache/hc/client5/http/impl/io/DefaultManagedHttpClientConnection.class" + ) + ), + StandardCharsets.ISO_8859_1 + ); + assertThat(connectionClass) + .contains("org.apache.hc.client5.http.wire") + .contains("org.apache.hc.client5.http.headers"); + } + private ListAssert assertThatFileList(Path path) throws IOException { return (ListAssert) assertThat(Files.list(path)) .extracting(Path::getFileName) diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml index 7d4d9e8f4b4..d8c72ff3d5f 100644 --- a/core/src/test/resources/logback-test.xml +++ b/core/src/test/resources/logback-test.xml @@ -19,4 +19,8 @@ + + + + diff --git a/docs/supported_docker_environment/logging_config.md b/docs/supported_docker_environment/logging_config.md index f9f511cfd6e..c0a43b3dfac 100644 --- a/docs/supported_docker_environment/logging_config.md +++ b/docs/supported_docker_environment/logging_config.md @@ -20,7 +20,14 @@ should be included in your classpath to show a reasonable level of log output: - + + + ``` @@ -30,4 +37,22 @@ In order to troubleshoot issues with Testcontainers, increase the logging level ``` -Avoid changing the root logger's level to `DEBUG`, because this turns on debug logging for every package whose level isn't explicitly configured here, resulting in a large amount of log data. +Raising `org.testcontainers` (or `com.github.dockerjava`) to `DEBUG` does not turn on HTTP wire logging for the +communication with the Docker daemon. That logging is off by default and opt-in only, because the wire logs include +complete HTTP request and response headers and bodies, including registry credentials (e.g. the Base64-encoded +`X-Registry-Auth` header used for image pulls). To enable it temporarily for troubleshooting, explicitly set the exact +logger names to `DEBUG`: + +```xml + + + +``` + +The same logger names apply whether Testcontainers is using its shaded copy of Apache HttpClient (from the published +jar) or a plain Apache HttpClient 5.x (e.g. when running from a source classpath), and they also control wire logging +for other Apache HttpClient 5.x usage in your application. + +Avoid changing the root logger's level to `DEBUG`, because this turns on debug logging for every package whose level +isn't explicitly configured here — resulting in a large amount of log data and, in particular, in wire logging of the +Docker API communication (see above), which includes credentials.