This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 0795c50df145f2b65f7b50c750e383c84db2b404 Author: Robert Lazarski <[email protected]> AuthorDate: Sat Sep 5 03:47:36 2026 -1000 Stop a remote server retargeting WSDL2Java's own WSDL fetch The tool probes an http location for a redirect before parsing, and took the Location header from any response: a stray header on a 200 redirected it, a relative target was used unresolved, and Location: file:///etc/passwd aimed the parse at the developer's filesystem. The redirect is the hostile document's server talking, not the developer. It is now followed only from a 3xx, only to http or https, resolved against the document requested; anything else stops codegen rather than quietly parsing something else. Timeouts added. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- SECURITY.md | 10 ++ .../axis2/wsdl/codegen/CodeGenConfiguration.java | 98 +++++++++++++++-- .../axis2/wsdl/codegen/WsdlRedirectTest.java | 120 +++++++++++++++++++++ src/site/markdown/release-notes/2.0.2.md | 9 ++ 4 files changed, 231 insertions(+), 6 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index e529a9b420..4a7f5de377 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -252,6 +252,16 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in locator keeps its own resolution behaviour, and a bare relative path still loads, as wsdl4j accepts. Screening must not narrow what can be loaded, or it breaks ordinary deployments rather than attacks. + - `WSDL2Java` probes an http location for a redirect before parsing it, and + that probe is the hostile server's input, not the developer's. It now + follows a redirect only from a 3xx status, only to http or https, and + resolves a relative target against the document requested. It previously + took `Location` from any response and used it verbatim, so a stray header on + a 200 retargeted the tool and `Location: file:///etc/passwd` aimed the parse + at the developer's own filesystem. A redirect elsewhere stops code + generation rather than being ignored, so the tool cannot quietly parse a + document other than the one named. Connect and read timeouts apply + (`axis2.codegen.wsdl.connect.timeout`, `axis2.codegen.wsdl.read.timeout`). - Not screened: `WSDL11ToAxisServiceBuilder.readInTheWSDLFile` when no resolver is supplied parses the top document with the hardened `XMLUtils.newDocument`, but wsdl4j fetches any `wsdl:import` itself. The diff --git a/modules/codegen/src/org/apache/axis2/wsdl/codegen/CodeGenConfiguration.java b/modules/codegen/src/org/apache/axis2/wsdl/codegen/CodeGenConfiguration.java index 05c5bf8f1b..17815f90bb 100644 --- a/modules/codegen/src/org/apache/axis2/wsdl/codegen/CodeGenConfiguration.java +++ b/modules/codegen/src/org/apache/axis2/wsdl/codegen/CodeGenConfiguration.java @@ -40,6 +40,8 @@ import javax.xml.namespace.QName; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; @@ -661,18 +663,102 @@ public class CodeGenConfiguration implements CommandLineOptionConstants { isUseOperationName = useOperationName; } + private static final int REDIRECT_CONNECT_TIMEOUT = + Integer.getInteger("axis2.codegen.wsdl.connect.timeout", 10000); + private static final int REDIRECT_READ_TIMEOUT = + Integer.getInteger("axis2.codegen.wsdl.read.timeout", 30000); + + /** + * The WSDL location as an http/https URL, or null if it is anything else -- + * a file path, a jar: URL, a classpath name. Only an http(s) document can be + * redirected, and only those are probed here. + * + * @param wsdlUri the location as given on the command line + * @return the URL to probe, or null to leave the location alone + */ + static URL asHttpUrl(String wsdlUri) { + if (wsdlUri == null) { + return null; + } + try { + URL url = new URL(wsdlUri); + String protocol = url.getProtocol(); + // "http" as a prefix test also matched schemes like "httpx"; ask the + // parsed URL instead of the string. + return "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol) + ? url : null; + } catch (MalformedURLException e) { + return null; + } + } + + /** + * Where a redirect points, or null if this response is not one to follow. + * <p> + * The Location header was previously taken from whatever the server answered, + * without looking at the status code and without constraining the target, so a + * 200 carrying a stray Location redirected the tool, and a hostile server could + * answer {@code Location: file:///etc/passwd} and retarget the parse at the + * developer's own filesystem. A redirect is followed only from a redirect + * status, only to http or https, and a relative target is resolved against the + * document that was asked for, as RFC 9110 requires. + * + * @param requestUrl the URL that was requested + * @param responseCode the status the server answered with + * @param location the Location header, or null + * @return the absolute http(s) target, or null to keep the original location + * @throws CodeGenerationException if a redirect points somewhere unusable, which + * is worth stopping for rather than silently + * parsing the wrong document + */ + static String redirectTarget(URL requestUrl, int responseCode, String location) + throws CodeGenerationException { + if (responseCode < 300 || responseCode > 399 || location == null) { + return null; + } + String trimmed = location.trim(); + if (trimmed.isEmpty()) { + return null; + } + URI target; + try { + target = requestUrl.toURI().resolve(trimmed); + } catch (URISyntaxException e) { + throw new CodeGenerationException( + "WSDL location " + requestUrl + " redirected to a target that is" + + " not a valid URI: " + trimmed, e); + } catch (IllegalArgumentException e) { + throw new CodeGenerationException( + "WSDL location " + requestUrl + " redirected to a target that is" + + " not a valid URI: " + trimmed, e); + } + String scheme = target.getScheme(); + if (scheme == null + || !("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme))) { + throw new CodeGenerationException( + "WSDL location " + requestUrl + " redirected to " + target + + "; only http and https redirects are followed. Fetch the" + + " document yourself if that target is what you intended."); + } + return target.toString(); + } + public void loadWsdl(String wsdlUri) throws CodeGenerationException { try { // the redirected urls gives problems in code generation some times with jaxbri // eg. https://www.paypal.com/wsdl/PayPalSvc.wsdl // if there is a redirect url better to find it and use. - if (wsdlUri.startsWith("http")) { - URL url = new URL(wsdlUri); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + URL requestUrl = asHttpUrl(wsdlUri); + if (requestUrl != null) { + HttpURLConnection connection = + (HttpURLConnection) requestUrl.openConnection(); connection.setInstanceFollowRedirects(false); - connection.getResponseCode(); - String newLocation = connection.getHeaderField("Location"); - if (newLocation != null){ + connection.setConnectTimeout(REDIRECT_CONNECT_TIMEOUT); + connection.setReadTimeout(REDIRECT_READ_TIMEOUT); + int responseCode = connection.getResponseCode(); + String newLocation = redirectTarget(requestUrl, responseCode, + connection.getHeaderField("Location")); + if (newLocation != null) { wsdlUri = newLocation; } } diff --git a/modules/codegen/test/org/apache/axis2/wsdl/codegen/WsdlRedirectTest.java b/modules/codegen/test/org/apache/axis2/wsdl/codegen/WsdlRedirectTest.java new file mode 100644 index 0000000000..0f16768c16 --- /dev/null +++ b/modules/codegen/test/org/apache/axis2/wsdl/codegen/WsdlRedirectTest.java @@ -0,0 +1,120 @@ +/* + * 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.axis2.wsdl.codegen; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +/** + * WSDL2Java probes an http location for a redirect before parsing it, because a + * redirected URL confuses some databindings. It took the Location header from + * whatever the server answered -- any status code -- and used it verbatim, so a + * hostile WSDL host could point the tool at a file: URL or a relative path it never + * resolved. The document being fetched is by definition one the developer does not + * control, so the redirect is the server's input, not the developer's. + */ +public class WsdlRedirectTest { + + private URL requested() throws Exception { + return new URL("https://vendor.example/svc?wsdl"); + } + + @Test + public void testAnOrdinaryRedirectIsFollowed() throws Exception { + assertEquals("https://vendor.example/real.wsdl", + CodeGenConfiguration.redirectTarget(requested(), 302, + "https://vendor.example/real.wsdl")); + assertEquals("https://vendor.example/real.wsdl", + CodeGenConfiguration.redirectTarget(requested(), 301, + "https://vendor.example/real.wsdl")); + assertEquals("https://vendor.example/real.wsdl", + CodeGenConfiguration.redirectTarget(requested(), 308, + "https://vendor.example/real.wsdl")); + } + + /** RFC 9110 allows a relative target; it was previously used as given. */ + @Test + public void testARelativeTargetIsResolvedAgainstTheRequest() throws Exception { + assertEquals("https://vendor.example/wsdl/real.wsdl", + CodeGenConfiguration.redirectTarget(requested(), 302, "/wsdl/real.wsdl")); + assertEquals("https://vendor.example/real.wsdl", + CodeGenConfiguration.redirectTarget(requested(), 302, "real.wsdl")); + } + + /** A Location on a non-redirect status is not a redirect. */ + @Test + public void testLocationOnANonRedirectStatusIsIgnored() throws Exception { + assertNull(CodeGenConfiguration.redirectTarget(requested(), 200, + "https://vendor.example/real.wsdl")); + assertNull(CodeGenConfiguration.redirectTarget(requested(), 404, + "https://vendor.example/real.wsdl")); + assertNull(CodeGenConfiguration.redirectTarget(requested(), 500, + "https://vendor.example/real.wsdl")); + } + + @Test + public void testNoLocationIsNoRedirect() throws Exception { + assertNull(CodeGenConfiguration.redirectTarget(requested(), 302, null)); + assertNull(CodeGenConfiguration.redirectTarget(requested(), 302, " ")); + } + + /** The retargeting case: a redirect must not aim the parse at the local disk. */ + @Test + public void testARedirectToANonHttpSchemeIsRefused() throws Exception { + assertRefused("file:///etc/passwd"); + assertRefused("jar:file:/tmp/a.jar!/x.wsdl"); + assertRefused("ftp://internal-host/x.wsdl"); + } + + private void assertRefused(String location) throws Exception { + try { + CodeGenConfiguration.redirectTarget(requested(), 302, location); + fail("should have refused the redirect to " + location); + } catch (CodeGenerationException expected) { + assertTrue(expected.getMessage().contains(location.substring(0, 4)), + () -> "should name the target, was: " + expected.getMessage()); + } + } + + /** Only http(s) documents are probed at all. */ + @Test + public void testOnlyHttpLocationsAreProbed() { + assertNotNull(CodeGenConfiguration.asHttpUrl("http://vendor.example/svc?wsdl")); + assertNotNull(CodeGenConfiguration.asHttpUrl("https://vendor.example/svc?wsdl")); + assertNull(CodeGenConfiguration.asHttpUrl("file:/tmp/a.wsdl")); + assertNull(CodeGenConfiguration.asHttpUrl("test-resources/wsdls/Version.wsdl")); + assertNull(CodeGenConfiguration.asHttpUrl(null)); + } + + /** + * The old test was a startsWith("http") on the raw string, which also matched a + * scheme merely beginning with those letters. + */ + @Test + public void testASchemeThatOnlyStartsWithHttpIsNotProbed() { + assertNull(CodeGenConfiguration.asHttpUrl("httpx://vendor.example/svc")); + } +} diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md index be07704afb..7bd095e8bd 100644 --- a/src/site/markdown/release-notes/2.0.2.md +++ b/src/site/markdown/release-notes/2.0.2.md @@ -164,6 +164,15 @@ in `SECURITY.md`. much as to SOAP faults. The reason is now generic unless details are enabled. The error is logged with its stack as before. +- **WSDL2Java no longer lets a remote server retarget its own WSDL fetch.** The tool + probes an http location for a redirect before parsing, and took the `Location` header + from any response, unresolved and unconstrained: a stray header on a `200` redirected + it, a relative target was used as given, and `Location: file:///etc/passwd` pointed the + parse at the developer's filesystem. A redirect is now followed only from a 3xx + status, only to http or https, and a relative target resolves against the document + requested; anything else stops code generation instead of silently parsing something + else. Connect and read timeouts are applied and configurable. + - **Packaged schema and WSDL documents are served only from the requesting service's own archive.** `AxisService.printXSD` and `printWSDL2` -- the `?xsd=` and `?wsdl2=` queries -- looked a requested name up with `getResourceAsStream`, which delegates
