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

Reply via email to