From 6263960cc2e34bb8c87193ef3253c52036ae2d7b Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Fri, 26 Jun 2026 15:51:14 +0200 Subject: [PATCH 1/2] Fix NO_PROXY hostname boundary matching --- .../client/utils/HttpClientUtils.java | 31 ++++++++++++++++--- .../client/utils/HttpClientUtilsTest.java | 24 ++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java index 3804a133ea6..c29527f0e27 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/HttpClientUtils.java @@ -37,6 +37,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; @@ -291,15 +292,37 @@ static boolean isHostMatchedByNoProxy(String host, String[] noProxies) throws Ma if (new IpAddressMatcher(noProxyIpOrSubnet.get()).matches(host)) { return true; } - } else { - if (host.endsWith(noProxy)) { - return true; - } + } else if (isHostnameMatchedByNoProxy(host, noProxy)) { + return true; } } return false; } + private static boolean isHostnameMatchedByNoProxy(String host, String noProxy) { + if (host == null || noProxy == null || noProxy.isEmpty()) { + return false; + } + String normalizedHost = normalizeHostnameForNoProxy(host); + String normalizedNoProxy = normalizeHostnameForNoProxy(noProxy); + if (normalizedNoProxy.startsWith(".")) { + normalizedNoProxy = normalizedNoProxy.substring(1); + } + if (normalizedNoProxy.isEmpty()) { + return false; + } + return normalizedHost.equals(normalizedNoProxy) + || normalizedHost.endsWith("." + normalizedNoProxy); + } + + private static String normalizeHostnameForNoProxy(String hostname) { + String normalizedHostname = hostname.toLowerCase(Locale.ROOT); + if (normalizedHostname.endsWith(".")) { + return normalizedHostname.substring(0, normalizedHostname.length() - 1); + } + return normalizedHostname; + } + private static Optional extractIpAddressOrSubnet(String ipAddressOrSubnet) { final Matcher ipMatcher = IP_PATTERN.matcher(ipAddressOrSubnet); if (ipMatcher.matches()) { diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java index f50d47154ce..dd19ec671c7 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/HttpClientUtilsTest.java @@ -104,6 +104,22 @@ void testConfigureProxyAuth() throws Exception { Mockito.verify(builder).proxyAuthorization("Basic dXNlcjpwYXNzd29yZA=="); } + @Test + void testConfigureProxyDoesNotBypassLookalikeNoProxySuffix() throws Exception { + Config config = new ConfigBuilder() + .withMasterUrl("https://evilcorp.example.") + .withHttpsProxy("http://proxy.internal:8080") + .withNoProxy("corp.example.") + .build(); + Builder builder = Mockito.mock(HttpClient.Builder.class, Mockito.RETURNS_SELF); + + HttpClientUtils.configureProxy(config, builder); + + Mockito.verify(builder).proxyAddress(new InetSocketAddress("proxy.internal", 8080)); + Mockito.verify(builder).proxyType(HttpClient.ProxyType.HTTP); + Mockito.verify(builder, Mockito.never()).proxyType(HttpClient.ProxyType.DIRECT); + } + @Test void testApplyCommonConfigurationWithTlsServerName() { // Given @@ -211,7 +227,10 @@ Stream masterHostnameDoesMatchNoProxyInput() { return Stream.of( arguments("192.168.1.100", new String[] { "192.168.1.0/24" }), arguments("master.example.com", new String[] { "master.example.com" }), + arguments("master.example.com.", new String[] { "master.example.com" }), + arguments("master.example.com", new String[] { "master.example.com." }), arguments("master.example.com", new String[] { ".example.com" }), + arguments("master.example.com.", new String[] { ".example.com." }), arguments("master.example.com", new String[] { "circleci-internal-outer-build-agent", "one.com", "other.com", ".com" }), arguments("192.168.1.110", new String[] { "192.168.1.110" }), @@ -239,6 +258,11 @@ Stream masterHostnameDoesNotMatchNoProxyInput() { arguments("master.example.com", new String[0]), arguments("master.example.com", new String[] { "master1.example.com" }), arguments("master.example.com", new String[] { ".example1.com" }), + arguments("evilcorp.example", new String[] { "corp.example" }), + arguments("evilcorp.example.", new String[] { "corp.example" }), + arguments("evilcorp.example", new String[] { "corp.example." }), + arguments("evilcorp.example.", new String[] { ".corp.example." }), + arguments("master.example.com", new String[] { "." }), arguments("master.example.com", new String[] { "circleci-internal-outer-build-agent", }), arguments("master.example.com", new String[] { "one.com", "other.com", "master.example.", ".co" }), arguments("192.168.1.110", new String[] { "192.168.1.111" }), From 2cf47730e721df2b40e19301df93e5fa954f9587 Mon Sep 17 00:00:00 2001 From: GrosQuildu Date: Fri, 26 Jun 2026 18:55:51 +0200 Subject: [PATCH 2/2] docs: update changelog for #7950 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 760c3671474..aed306e67c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### 7.8-SNAPSHOT #### Bugs +* Fix #7950: (kubernetes-client-api) `NO_PROXY` hostname entries now match only exact hosts or dot-delimited subdomains, preventing lookalike suffixes from bypassing the configured proxy * Fix #7953: (httpclient-jdk) bodyless requests now preserve the requested HTTP method instead of silently defaulting to `GET`. `JdkHttpClientImpl.requestBuilder` only called `HttpRequest.Builder.method(...)` inside the `body != null` branch, so a bodyless `DELETE`/`POST`/`PUT`/`PATCH` (such as `client.raw(uri, "DELETE", null)`) was sent as `GET` on the JDK backend; the method is now set with `BodyPublishers.noBody()` when there is no body, matching the OkHttp, Jetty and Vert.x backends * Fix #7435: (kubernetes-client) A `SharedIndexInformer`'s periodic resync no longer stops permanently and silently when a single resync cycle throws. `DefaultSharedIndexInformer.scheduleResync` runs the resync through `Utils.scheduleAtFixedRate`, whose self-rescheduling chain re-arms the next cycle only when the previous one completes normally; an uncaught exception completed the (unobserved) `resyncFuture` exceptionally and the resync was never scheduled again, with no log, while the independent watch kept `isWatching()` reporting `true` (a restart was required to recover). The resync command now catches and `WARN`-logs the failure so the schedule fires again at the next interval * Fix #7933: (kubernetes-client-api) Deterministic TLS trust failures (untrusted cert, expired cert, hostname mismatch) are now classified as terminal and fail fast instead of being retried by the shared `StandardHttpClient.shouldRetry` backoff loop (~19 s drain). The classifier walks both `getCause()` and `getSuppressed()` trees for `CertificateException`, `CertPathValidatorException`, `CertPathBuilderException`, and `SSLPeerUnverifiedException`. Affects all five HTTP client modules (jdk, jetty, okhttp, vertx-4, vertx-5) on both the HTTP request and WebSocket connect paths