CVE-2026-25534
Description
Impact
Spinnaker updated URL Validation logic on user input to provide sanitation on user inputted URLs for clouddriver. However, they missed that Java URL objects do not correctly handle underscores on parsing. This led to a bypass of the previous CVE (CVE-2025-61916) through the use of carefully crafted URLs. Note, Spinnaker found this not just in that CVE, but in the existing URL validations in Orca fromUrl expression handling. This CVE impacts BOTH artifacts as a result.
Patches
This has been merged and will be available in versions 2025.4.1, 2025.3.1, 2025.2.4 and 2026.0.0.
Workarounds
You can disable the various artifacts on this system to work around these limits.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
io.spinnaker.clouddriver:clouddriver-artifactsMaven | < 2025.2.4 | 2025.2.4 |
io.spinnaker.clouddriver:clouddriver-artifactsMaven | >= 2025.3.0, < 2025.3.1 | 2025.3.1 |
io.spinnaker.clouddriver:clouddriver-artifactsMaven | >= 2025.4.0, < 2025.4.1 | 2025.4.1 |
io.spinnaker.orca:orca-coreMaven | < 2025.2.4 | 2025.2.4 |
io.spinnaker.orca:orca-coreMaven | >= 2025.3.0, < 2025.3.1 | 2025.3.1 |
io.spinnaker.orca:orca-coreMaven | >= 2025.4.0, < 2025.4.1 | 2025.4.1 |
Affected products
1Patches
17c4737906239fix(validation): Fixes some url validation handling on underscores (#7428)
6 files changed · +81 −55
clouddriver/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/config/BaseHttpArtifactCredentials.java+1 −1 modified@@ -75,7 +75,7 @@ protected HttpUrl parseUrl(String stringUrl) { + ". Read more here https://www.spinnaker.io/reference/artifacts/types/"); } if (account.getUrlRestrictions() != null) { - account.getUrlRestrictions().validateURI(httpUrl.uri().normalize()); + account.getUrlRestrictions().validateURI(httpUrl); } return httpUrl; }
clouddriver/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/config/HttpUrlRestrictions.java+6 −19 modified@@ -32,8 +32,10 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import okhttp3.HttpUrl; import org.apache.commons.lang3.StringUtils; import org.springframework.security.web.util.matcher.IpAddressMatcher; +import org.springframework.util.ObjectUtils; /** * A set of restrictions and validations of the restrictions. These in combination provide some @@ -154,9 +156,9 @@ boolean isValidIpAddress(String host) { .noneMatch(restriction -> new IpAddressMatcher(restriction).matches(host)); } - public URI validateURI(URI url) throws IllegalArgumentException { + public URI validateURI(HttpUrl url) throws IllegalArgumentException { try { - URI u = url.normalize(); + URI u = url.uri().normalize(); if (!u.isAbsolute()) { throw new IllegalArgumentException("non absolute URI " + url); } @@ -166,21 +168,9 @@ public URI validateURI(URI url) throws IllegalArgumentException { // fallback to `getAuthority()` in the event that the hostname contains an underscore and // `getHost()` returns null - String host = u.getHost(); - if (host == null) { - String authority = u.getAuthority(); - if (authority != null) { - // Don't attempt to colon-substring ipv6 addresses - if (InetAddresses.isInetAddress(authority)) { - host = authority; - } else { - int portIndex = authority.indexOf(":"); - host = (portIndex > -1) ? authority.substring(0, portIndex) : authority; - } - } - } + String host = url.host(); - if (host == null || host.isEmpty()) { + if (ObjectUtils.isEmpty(host)) { throw new IllegalArgumentException("Unable to determine host for the url provided " + url); } @@ -190,9 +180,6 @@ public URI validateURI(URI url) throws IllegalArgumentException { } // Strip ipv6 brackets if present - // InetAddress.getHost() retains them, but other code doesn't quite understand - host = host.replace("[", "").replace("]", ""); - if (InetAddresses.isInetAddress(host) && rejectVerbatimIps) { throw new IllegalArgumentException("Verbatim IP addresses are not allowed"); }
clouddriver/clouddriver-artifacts/src/test/java/com/netflix/spinnaker/clouddriver/artifacts/config/HttpUrlRestrictionsTest.java+26 −17 modified@@ -3,8 +3,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.net.URI; import java.util.List; +import okhttp3.HttpUrl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -19,10 +19,10 @@ public void verifyThatIpRangesBlockAccess() { .build(); assertThrows( IllegalArgumentException.class, - () -> restrictions.validateURI(URI.create("http://192.168.0.1"))); + () -> restrictions.validateURI(HttpUrl.parse("http://192.168.0.1"))); assertThrows( IllegalArgumentException.class, - () -> restrictions.validateURI(URI.create("http://10.2.3.4"))); + () -> restrictions.validateURI(HttpUrl.parse("http://10.2.3.4"))); } @Test @@ -31,10 +31,10 @@ public void blockVerbatimIpsWhenNoIpListSet() { HttpUrlRestrictions.builder().rejectVerbatimIps(true).rejectedIps(List.of()).build(); assertThrows( IllegalArgumentException.class, - () -> restrictions.validateURI(URI.create("http://192.168.0.1"))); + () -> restrictions.validateURI(HttpUrl.parse("http://192.168.0.1"))); assertThrows( IllegalArgumentException.class, - () -> restrictions.validateURI(URI.create("http://10.2.3.4"))); + () -> restrictions.validateURI(HttpUrl.parse("http://10.2.3.4"))); } @Test @@ -44,17 +44,18 @@ public void testWHenNoAllowedRegexButWhiteListIsSet() { .allowedHostnamesRegex("") .allowedDomains(List.of("google.com")) .build(); - assertThat(restrictions.validateURI(URI.create("http://google.com"))).hasHost("google.com"); + assertThat(restrictions.validateURI(HttpUrl.parse("http://google.com"))).hasHost("google.com"); assertThrows( IllegalArgumentException.class, - () -> restrictions.validateURI(URI.create("http://microsoft.com"))); + () -> restrictions.validateURI(HttpUrl.parse("http://microsoft.com"))); } @Test public void allowIpsWhenVerbatimIpsIsFalse() { var restrictions = HttpUrlRestrictions.builder().rejectVerbatimIps(false).rejectedIps(List.of()).build(); - assertThat(restrictions.validateURI(URI.create("http://192.168.0.1"))).hasHost("192.168.0.1"); + assertThat(restrictions.validateURI(HttpUrl.parse("http://192.168.0.1"))) + .hasHost("192.168.0.1"); } @Test @@ -68,20 +69,26 @@ public void blockIPRangesWhenResolved() { IllegalArgumentException.class, () -> restrictions.validateURI( - URI.create( + HttpUrl.parse( "http://0a010203.0a010204.rbndr.us"))); // Make sure a host lookup that returns // a 10. address ALSO fails when // restricted. - assertThat(restrictions.validateURI(URI.create("http://google.com"))).hasHost("google.com"); + assertThat(restrictions.validateURI(HttpUrl.parse("http://google.com"))).hasHost("google.com"); } @Test public void whiteListBlockEverythingElse() { var restrictions = HttpUrlRestrictions.builder().allowedDomains(List.of("example.com")).build(); assertThrows( IllegalArgumentException.class, - () -> restrictions.validateURI(URI.create("http://google.com"))); - assertThat(restrictions.validateURI(URI.create("http://example.com"))).hasHost("example.com"); + () -> restrictions.validateURI(HttpUrl.parse("http://google.com"))); + assertThrows( + IllegalArgumentException.class, + () -> + restrictions.validateURI( + HttpUrl.parse("http://example.com:password@some_underscore_host.com"))); + assertThat(restrictions.validateURI(HttpUrl.parse("http://example.com"))) + .hasHost("example.com"); } @Test @@ -91,9 +98,9 @@ public void allowAHostIfResolvesInIpList() { .rejectVerbatimIps(true) .rejectedIps(List.of("192.168.0.0/16")) .build(); - assertThat(restrictions.validateURI(URI.create("http://0a010203.0a010204.rbndr.us"))) + assertThat(restrictions.validateURI(HttpUrl.parse("http://0a010203.0a010204.rbndr.us"))) .hasHost("0a010203.0a010204.rbndr.us"); - assertThat(restrictions.validateURI(URI.create("http://google.com"))).hasHost("google.com"); + assertThat(restrictions.validateURI(HttpUrl.parse("http://google.com"))).hasHost("google.com"); } @Test @@ -112,11 +119,13 @@ void blockDefaultRestrictedDomains() { invalidDomains.forEach( domain -> { Assertions.assertThrows( - IllegalArgumentException.class, () -> restrictions.validateURI(URI.create(domain))); + IllegalArgumentException.class, + () -> restrictions.validateURI(HttpUrl.parse(domain))); }); - assertThat(restrictions.validateURI(URI.create("http://example.com"))).hasHost("example.com"); - assertThat(restrictions.validateURI(URI.create("http://0a010203.0a010204.rbndr.us"))) + assertThat(restrictions.validateURI(HttpUrl.parse("http://example.com"))) + .hasHost("example.com"); + assertThat(restrictions.validateURI(HttpUrl.parse("http://0a010203.0a010204.rbndr.us"))) .hasHost("0a010203.0a010204.rbndr.us"); } }
orca/orca-core/src/main/java/com/netflix/spinnaker/orca/config/UserConfiguredUrlRestrictions.java+8 −15 modified@@ -36,6 +36,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import okhttp3.HttpUrl; import org.apache.commons.lang3.StringUtils; import org.springframework.security.web.util.matcher.IpAddressMatcher; @@ -217,9 +218,13 @@ boolean isIpAddress(String host) { return InetAddresses.isInetAddress(host); } - public URI validateURI(String url) throws IllegalArgumentException { + public URI validateURI(String uri) throws IllegalArgumentException { + return validateURI(HttpUrl.parse(uri)); + } + + public URI validateURI(HttpUrl url) throws IllegalArgumentException { try { - URI u = URI.create(url).normalize(); + URI u = url.uri().normalize(); if (!u.isAbsolute()) { throw new IllegalArgumentException("non absolute URI " + url); } @@ -229,19 +234,7 @@ public URI validateURI(String url) throws IllegalArgumentException { // fallback to `getAuthority()` in the event that the hostname contains an underscore and // `getHost()` returns null - String host = u.getHost(); - if (host == null) { - String authority = u.getAuthority(); - if (authority != null) { - // Don't attempt to colon-substring ipv6 addresses - if (isIpAddress(authority)) { - host = authority; - } else { - int portIndex = authority.indexOf(":"); - host = (portIndex > -1) ? authority.substring(0, portIndex) : authority; - } - } - } + String host = url.host(); if (host == null || host.isEmpty()) { throw new IllegalArgumentException("Unable to determine host for the url provided " + url);
orca/orca-core/src/test/groovy/com/netflix/spinnaker/orca/config/UserConfiguredUrlRestrictionsSpec.groovy+37 −2 modified@@ -166,6 +166,39 @@ class UserConfiguredUrlRestrictionsSpec extends Specification { 'https://[fd12:3456:789a:1::1]:8080' ] } + @Unroll + def 'validate when authority is used to try to bypass validation'() { + given: + UserConfiguredUrlRestrictions config = spyOn(new UserConfiguredUrlRestrictions.Builder().withAllowedHostnamesRegex("example.com").build()) + + when: + config.validateURI(uri) + + then: + thrown(IllegalArgumentException.class) + + where: + uri << [ + 'https://example.com:badpassword@host_with_underscore.com' + ] + } + @Unroll + def 'validate when authority is used to try to bypass validation'() { + given: + UserConfiguredUrlRestrictions config = spyOn(new UserConfiguredUrlRestrictions.Builder().withAllowedHostnamesRegex("host_with_underscore.com").build()) + + when: + URI validatedUri = config.validateURI(uri) + + then: + noExceptionThrown() + validatedUri + + where: + uri << [ + 'https://example.com:badpassword@host_with_underscore.com' + ] + } @Unroll def 'allows verbatim IP addresses if configured'() { @@ -186,8 +219,10 @@ class UserConfiguredUrlRestrictionsSpec extends Specification { 'https://192.168.0.1', 'http://172.16.0.1', 'http://10.0.0.1', - 'https://fd12:3456:789a:1::1', - 'https://fc12:3456:789a:1::1', + // IPV6 when using HttpUrl - which we use for validation - requires the ipv6 raw addresses to be quoted correctly. OTherwise you get into some dangerous parsing issues + 'https://example.com:badpassword@[fd12:3456:789a:1::1]:8080', +// 'https://fd12:3456:789a:1::1', +// 'https://fc12:3456:789a:1::1', 'https://[fd12:3456:789a:1::1]:8080', 'https://[fc12:3456:789a:1::1]:8080' ]
orca/orca-webhook/src/test/java/com/netflix/spinnaker/orca/webhook/service/WebhookServiceTest.java+3 −1 modified@@ -132,7 +132,9 @@ void testAllowedRequestsEnabledTrueEmptyList() { Optional.empty(), providedIdRequestFilterConfigurationProperties); - String url = "https://localhost"; // arbitrary, but needs to include a resolvable hostname + String url = + "https://localhost/"; // arbitrary, but needs to include a resolvable hostname. MUST be a + // normalized URI for tests to work // The StageExecutionImpl constructor mutates the map, so use a mutable map. Map<String, Object> webhookStageData =
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-8r8j-gfhg-fw38ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25534ghsaADVISORY
- github.com/spinnaker/spinnaker/commit/7c4737906239a958a468e843239c6785b03d0edanvdWEB
- github.com/spinnaker/spinnaker/security/advisories/GHSA-8r8j-gfhg-fw38nvdWEB
- github.com/spinnaker/spinnaker/security/advisories/GHSA-vrjc-q2fh-6x9hnvdWEB
News mentions
0No linked articles in our index yet.