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 6bdd6a07eaba04496730b95bb6c34bcb9434e496
Author: Robert Lazarski <[email protected]>
AuthorDate: Fri Sep 4 13:09:41 2026 -1000

    Screen every WSDL-loading path, not just the client-side one
    
    wsdl4j parses with its own unhardened parser and fetches the import chain
    itself, so a DOCTYPE anywhere in a WSDL was an XXE in whichever JVM read it.
    SecureWSDLLocator screened only createClientSideAxisService, and it fetches
    over http/https alone, so it cannot serve the paths that load from a file, 
an
    archive, the classpath or a catalog. HardenedWSDLLocator separates screening
    from resolution: a delegate locator keeps its own catalog or archive 
behaviour
    and only its bytes are screened, and a bare relative path still loads, 
which is
    what wsdl4j accepts and what codegen passes. Wired into the JAX-WS loader, 
the
    runtime reload wrapper, the deployment builder's resolver path and 
WSDL2Java.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 SECURITY.md                                        |  27 ++-
 .../axis2/wsdl/codegen/CodeGenConfiguration.java   |   9 +-
 .../description/WSDL11ToAxisServiceBuilder.java    |  14 +-
 .../org/apache/axis2/util/HardenedWSDLLocator.java | 245 +++++++++++++++++++++
 .../axis2/wsdl/util/WSDLWrapperReloadImpl.java     |   9 +-
 .../apache/axis2/util/HardenedWSDLLocatorTest.java | 171 ++++++++++++++
 .../org/apache/axis2/jaxws/util/WSDL4JWrapper.java |  14 +-
 src/site/markdown/release-notes/2.0.2.md           |   9 +
 8 files changed, 488 insertions(+), 10 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index 1feeae5d1b..7d0f123750 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -131,7 +131,7 @@ Axis2 exposes the following URL patterns from the servlet 
mapping:
 | Component | Threats | Mitigations |
 |-----------|---------|-------------|
 | **XML parsers** (AXIOM/StAX, DocumentBuilderFactory) | XXE, billion laughs, 
entity expansion DoS | DOCTYPE disallowed; external entities disabled; 
`DefaultEntityResolver` returns empty source |
-| **WSDL/XSD import resolution** (wsdl4j, xmlschema-core) | XXE in imported 
documents; SSRF via `file://`/`gopher://` schemes | `SecureWSDLLocator` 
pre-validates with hardened SAX parser; protocol whitelist (HTTP/HTTPS only); 
size limit (10MB default); connect/read timeouts; relative-path SSRF bypass 
blocked |
+| **WSDL/XSD import resolution** (wsdl4j, xmlschema-core) | XXE in imported 
documents; SSRF via `file://`/`gopher://` schemes | `SecureWSDLLocator` 
pre-validates the client-side path with a hardened SAX parser (HTTP/HTTPS only, 
10MB default, connect/read timeouts, relative-path SSRF bypass blocked); 
`HardenedWSDLLocator` screens the file, archive, classpath and catalog paths, 
refusing a DOCTYPE without restricting where a document may be loaded from |
 | **JSON parser** (Gson) | Deep nesting stack exhaustion, large payload DoS | 
Fuzz-tested (1.7M+ iterations); Gson nesting limits |
 | **JSON-RPC dispatch** | Method name injection; unexpected operation 
invocation | Method names validated against deployed operations; unknown 
methods return fault |
 | **Multipart/file upload** (commons-fileupload2) | Unbounded file count DoS 
(CVE-2023-24998 pattern); unbounded body size; temp-file accumulation | 
commons-fileupload2 enforces the file count limit; `multipartMaxRequestSize` / 
`multipartMaxFileSize` bound the body; temp files are deleted immediately for 
form fields and tracked to collection for file parts |
@@ -220,10 +220,27 @@ migration from `commons-fileupload` 1.x to 
`commons-fileupload2` in
    instances created by the framework disable DTDs and external entities
    (`XMLUtils.java`, `SecureWSDLLocator.java`, `DefaultEntityResolver.java`).
 
-2. **WSDL import security:** `SecureWSDLLocator` pre-parses imported
-   documents before passing them to wsdl4j. Protocol-restricted to
-   HTTP/HTTPS. Size-limited. Timeout-protected. Relative-path SSRF
-   bypass patched.
+2. **WSDL import security (extended in 2.0.2):** wsdl4j parses with its own
+   unhardened parser and fetches the whole import chain itself, so every
+   document has to be screened before it reaches wsdl4j.
+
+   - `SecureWSDLLocator` screens the client-side path
+     (`createClientSideAxisService`): protocol-restricted to HTTP/HTTPS,
+     size-limited, timeout-protected, relative-path SSRF bypass patched.
+   - `HardenedWSDLLocator` screens the paths that load a WSDL from a file, an
+     archive, the classpath or a catalog, where an HTTP-only fetcher could not
+     be used: JAX-WS WSDL loading, the runtime reload wrapper, the deployment
+     builder's resolver path, and the codegen entry point. It refuses a DOCTYPE
+     in the document or in anything it imports, bounds size and applies
+     timeouts, but does not decide where a document comes from -- a delegate
+     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.
+   - 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
+     deployment callers do supply a resolver; a caller that does not, with a
+     remote base URI, is outside what is screened.
 
 3. **Schema import security:** URI resolvers for AAR and WAR deployments
    block HTTP/HTTPS/FTP/JAR/file scheme resolution to prevent SSRF via
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 b824977971..05c5bf8f1b 100644
--- 
a/modules/codegen/src/org/apache/axis2/wsdl/codegen/CodeGenConfiguration.java
+++ 
b/modules/codegen/src/org/apache/axis2/wsdl/codegen/CodeGenConfiguration.java
@@ -19,6 +19,7 @@
 
 package org.apache.axis2.wsdl.codegen;
 
+import org.apache.axis2.util.HardenedWSDLLocator;
 import org.apache.axis2.AxisFault;
 import org.apache.axis2.description.AxisService;
 import org.apache.axis2.description.WSDL11ToAllAxisServicesBuilder;
@@ -761,7 +762,13 @@ public class CodeGenConfiguration implements 
CommandLineOptionConstants {
         WSDLReader reader = 
WSDLUtil.newWSDLReaderWithPopulatedExtensionRegistry();
         reader.setFeature("javax.wsdl.importDocuments", true);
 
-        return reader.readWSDL(uri);
+        // The tool's whole purpose is to consume a contract somebody else 
wrote, so
+        // this document and every wsdl:import in its chain are untrusted 
input. Left
+        // to wsdl4j they are parsed with entities enabled: a DOCTYPE in a 
vendor's
+        // WSDL would read local files on the developer's machine and 
exfiltrate them
+        // through an external entity. Imports stay enabled -- a split WSDL is
+        // ordinary -- but each document is screened first.
+        return reader.readWSDL(new HardenedWSDLLocator(uri));
         
     }
 }
diff --git 
a/modules/kernel/src/org/apache/axis2/description/WSDL11ToAxisServiceBuilder.java
 
b/modules/kernel/src/org/apache/axis2/description/WSDL11ToAxisServiceBuilder.java
index 7a8eb37e27..19a0bfdcf8 100644
--- 
a/modules/kernel/src/org/apache/axis2/description/WSDL11ToAxisServiceBuilder.java
+++ 
b/modules/kernel/src/org/apache/axis2/description/WSDL11ToAxisServiceBuilder.java
@@ -31,6 +31,7 @@ import org.apache.axis2.addressing.EndpointReferenceHelper;
 import org.apache.axis2.addressing.wsdl.WSDL11ActionHelper;
 import org.apache.axis2.engine.AxisConfiguration;
 import org.apache.axis2.kernel.http.HTTPConstants;
+import org.apache.axis2.util.HardenedWSDLLocator;
 import org.apache.axis2.util.LoggingControl;
 import org.apache.axis2.util.PolicyUtil;
 import org.apache.axis2.util.XMLUtils;
@@ -2322,13 +2323,24 @@ public class WSDL11ToAxisServiceBuilder extends 
WSDLToAxisServiceBuilder {
         // imports are safe and necessary. The remote-URL code path in
         // AxisService.createClientSideAxisService() defaults to false and uses
         // a hardened WSDLLocator when opt-in is enabled.
+        //
+        // Note what this method does and does not enforce. Where a resolver is
+        // supplied -- which the deployment callers do -- both the document 
and its
+        // imports are screened, below. On the resolver-less branch only the 
top
+        // document is (XMLUtils.newDocument is hardened); wsdl4j fetches any
+        // wsdl:import itself, and nothing here checks that the input really is
+        // local. A caller handing this method a remote base URI without a 
resolver
+        // is outside what is screened.
         reader.setFeature("javax.wsdl.importDocuments", true);
 
         Definition def;
         // if the custem resolver is present then use it
         if (customWSDLResolver != null) {
+            // The deployment resolvers (AAR/WAR) resolve from inside the 
archive and
+            // keep doing so; wrapping them only refuses a DOCTYPE in what they
+            // return, which wsdl4j would otherwise parse with entities 
enabled.
             // make sure the wsdl definition has the URI for the base document 
set
-            def = reader.readWSDL(customWSDLResolver);
+            def = reader.readWSDL(new HardenedWSDLLocator(customWSDLResolver));
             def.setDocumentBaseURI(customWSDLResolver.getBaseURI());
             return def;
         } else {
diff --git a/modules/kernel/src/org/apache/axis2/util/HardenedWSDLLocator.java 
b/modules/kernel/src/org/apache/axis2/util/HardenedWSDLLocator.java
new file mode 100644
index 0000000000..a18b0c1df1
--- /dev/null
+++ b/modules/kernel/src/org/apache/axis2/util/HardenedWSDLLocator.java
@@ -0,0 +1,245 @@
+/*
+ * 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.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+
+import javax.wsdl.xml.WSDLLocator;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+
+/**
+ * A {@link WSDLLocator} that refuses a DOCTYPE declaration in the WSDL, or in
+ * anything it imports, before wsdl4j sees it.
+ * <p>
+ * wsdl4j parses with its own unhardened {@code DocumentBuilderFactory}, and it
+ * fetches the whole import chain itself, so a document reaching it unscreened 
is an
+ * XXE in the server JVM. {@link SecureWSDLLocator} screens the client-side 
path, but
+ * it fetches over http/https only -- deliberately, since that is all that 
path needs
+ * -- so it cannot be used where a WSDL is loaded from a file, an archive, a
+ * classpath resource or a catalog, which is what the deployment and runtime 
paths do.
+ * <p>
+ * This one separates the two concerns. It never decides <em>where</em> a 
document
+ * comes from: either a delegate locator resolves it, keeping whatever catalog 
or
+ * archive behaviour that locator implements, or it is read from its URI with 
any
+ * scheme the JVM supports. What it does in both cases is validate the bytes 
with a
+ * hardened SAX parse first, and hand wsdl4j a stream over the validated 
bytes. So
+ * hardening does not narrow what can be loaded.
+ */
+public class HardenedWSDLLocator implements WSDLLocator {
+
+    private static final Log log = 
LogFactory.getLog(HardenedWSDLLocator.class);
+
+    private static final int CONNECT_TIMEOUT =
+            Integer.getInteger("axis2.wsdl.import.connect.timeout", 5000);
+    private static final int READ_TIMEOUT =
+            Integer.getInteger("axis2.wsdl.import.read.timeout", 15000);
+    private static final long MAX_SIZE =
+            Long.getLong("axis2.wsdl.import.maxsize", 10 * 1024 * 1024);
+
+    private final WSDLLocator delegate;
+    private final String baseURI;
+    private String latestImportURI;
+
+    /**
+     * Validates whatever a locator already in use resolves, leaving its 
resolution
+     * behaviour -- catalogs, archives, the classpath -- exactly as it was.
+     *
+     * @param delegate the locator to wrap
+     */
+    public HardenedWSDLLocator(WSDLLocator delegate) {
+        this.delegate = delegate;
+        this.baseURI = delegate.getBaseURI();
+    }
+
+    /**
+     * Reads from a URI with any scheme the JVM supports, which is what the 
call
+     * sites replaced by this used to let wsdl4j do for itself.
+     *
+     * @param baseURI the WSDL location
+     */
+    public HardenedWSDLLocator(String baseURI) {
+        this.delegate = null;
+        this.baseURI = baseURI;
+    }
+
+    public InputSource getBaseInputSource() {
+        if (delegate != null) {
+            return validated(delegate.getBaseInputSource(), baseURI);
+        }
+        return validated(read(baseURI), baseURI);
+    }
+
+    public String getBaseURI() {
+        return baseURI;
+    }
+
+    public InputSource getImportInputSource(String parentLocation, String 
importLocation) {
+        if (delegate != null) {
+            InputSource source = delegate.getImportInputSource(parentLocation, 
importLocation);
+            latestImportURI = delegate.getLatestImportURI();
+            return validated(source, latestImportURI);
+        }
+        String resolved = resolve(parentLocation, importLocation);
+        latestImportURI = resolved;
+        return validated(read(resolved), resolved);
+    }
+
+    public String getLatestImportURI() {
+        return latestImportURI;
+    }
+
+    public void close() {
+        if (delegate != null) {
+            delegate.close();
+        }
+    }
+
+    /**
+     * Reads the source fully, rejects a DOCTYPE, and returns a fresh stream 
over the
+     * validated bytes. The source has to be consumed to be checked, which is 
why the
+     * bytes are buffered and replayed rather than handed on directly.
+     */
+    private InputSource validated(InputSource source, String systemId) {
+        if (source == null) {
+            return null;
+        }
+        try {
+            byte[] content = drain(source);
+
+            SAXParserFactory spf = SAXParserFactory.newInstance();
+            spf.setNamespaceAware(true);
+            
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl";, true);
+            
spf.setFeature("http://xml.org/sax/features/external-general-entities";, false);
+            
spf.setFeature("http://xml.org/sax/features/external-parameter-entities";, 
false);
+            spf.setXIncludeAware(false);
+            XMLReader xmlReader = spf.newSAXParser().getXMLReader();
+            xmlReader.setEntityResolver(new DefaultEntityResolver());
+            xmlReader.parse(new InputSource(new 
ByteArrayInputStream(content)));
+
+            InputSource validatedSource = new InputSource(new 
ByteArrayInputStream(content));
+            validatedSource.setSystemId(systemId != null ? systemId : 
source.getSystemId());
+            return validatedSource;
+        } catch (Exception e) {
+            // Fail closed. A WSDL that will not survive a hardened parse is 
not one
+            // to hand to a parser that resolves entities.
+            throw new RuntimeException("Refusing WSDL document " + systemId
+                    + ": it did not pass a hardened parse", e);
+        }
+    }
+
+    private byte[] drain(InputSource source) throws IOException {
+        InputStream in = source.getByteStream();
+        if (in == null) {
+            if (source.getSystemId() == null) {
+                throw new IOException("The locator returned a source with no 
stream"
+                        + " and no system id");
+            }
+            in = openStream(source.getSystemId());
+        }
+        try {
+            return readBounded(in, source.getSystemId());
+        } finally {
+            in.close();
+        }
+    }
+
+    private InputSource read(String uri) {
+        try {
+            InputStream in = openStream(uri);
+            try {
+                InputSource source = new InputSource(
+                        new ByteArrayInputStream(readBounded(in, uri)));
+                source.setSystemId(uri);
+                return source;
+            } finally {
+                in.close();
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to load WSDL document " + uri, 
e);
+        }
+    }
+
+    /**
+     * Opens a location the way wsdl4j's own {@code readWSDL(String)} would: 
as a URL
+     * if it is one, otherwise as a file path. Codegen and the deployment 
paths hand
+     * over bare relative paths, so accepting only URLs here would refuse the 
ordinary
+     * case rather than the dangerous one.
+     */
+    private InputStream openStream(String uri) throws IOException {
+        URL url;
+        try {
+            url = new URL(uri);
+        } catch (MalformedURLException notAUrl) {
+            File file = new File(uri);
+            if (!file.isFile()) {
+                throw new IOException("WSDL location is neither a URL nor a"
+                        + " readable file: " + uri, notAUrl);
+            }
+            url = file.toURI().toURL();
+        }
+        URLConnection connection = url.openConnection();
+        connection.setConnectTimeout(CONNECT_TIMEOUT);
+        connection.setReadTimeout(READ_TIMEOUT);
+        return connection.getInputStream();
+    }
+
+    private byte[] readBounded(InputStream in, String uri) throws IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        byte[] buffer = new byte[8192];
+        long total = 0;
+        int read;
+        while ((read = in.read(buffer)) != -1) {
+            total += read;
+            if (MAX_SIZE >= 0 && total > MAX_SIZE) {
+                throw new IOException("WSDL document " + uri + " exceeds "
+                        + MAX_SIZE + " bytes");
+            }
+            out.write(buffer, 0, read);
+        }
+        return out.toByteArray();
+    }
+
+    private String resolve(String parentLocation, String importLocation) {
+        try {
+            if (parentLocation == null) {
+                return importLocation;
+            }
+            return 
URI.create(parentLocation).resolve(importLocation).toString();
+        } catch (IllegalArgumentException e) {
+            if (log.isDebugEnabled()) {
+                log.debug("Could not resolve " + importLocation + " against "
+                        + parentLocation + "; using it as given");
+            }
+            return importLocation;
+        }
+    }
+}
diff --git 
a/modules/kernel/src/org/apache/axis2/wsdl/util/WSDLWrapperReloadImpl.java 
b/modules/kernel/src/org/apache/axis2/wsdl/util/WSDLWrapperReloadImpl.java
index 5b9c4eb571..496a9c5d1d 100644
--- a/modules/kernel/src/org/apache/axis2/wsdl/util/WSDLWrapperReloadImpl.java
+++ b/modules/kernel/src/org/apache/axis2/wsdl/util/WSDLWrapperReloadImpl.java
@@ -19,6 +19,7 @@
 
 package org.apache.axis2.wsdl.util;
 
+import org.apache.axis2.util.HardenedWSDLLocator;
 import org.apache.axis2.java.security.AccessController;
 import org.apache.axis2.util.JavaUtils;
 import org.apache.axis2.wsdl.WSDLUtil;
@@ -1422,7 +1423,13 @@ public class WSDLWrapperReloadImpl implements 
WSDLWrapperImpl {
                 def = (Definition) AccessController.doPrivileged(new 
PrivilegedExceptionAction() {
                     public Object run() throws WSDLException {
                         WSDLReader reader = getWSDLReader();
-                        return reader.readWSDL(wsdlExplicitURI);
+                        // This re-reads the WSDL at runtime, repeatedly, long
+                        // after any deployment-time screening. Reading it 
through
+                        // the hardened locator refuses a DOCTYPE in the 
document
+                        // or in anything it imports, without narrowing which
+                        // schemes can be loaded.
+                        return reader.readWSDL(
+                                new HardenedWSDLLocator(wsdlExplicitURI));
                     }
                 });
             } catch (PrivilegedActionException e) {
diff --git 
a/modules/kernel/test/org/apache/axis2/util/HardenedWSDLLocatorTest.java 
b/modules/kernel/test/org/apache/axis2/util/HardenedWSDLLocatorTest.java
new file mode 100644
index 0000000000..b1a3ebf2d4
--- /dev/null
+++ b/modules/kernel/test/org/apache/axis2/util/HardenedWSDLLocatorTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.charset.StandardCharsets;
+
+import javax.wsdl.xml.WSDLLocator;
+
+import junit.framework.TestCase;
+
+import org.xml.sax.InputSource;
+
+/**
+ * wsdl4j parses with its own unhardened parser and fetches imports itself, so 
a
+ * document has to be screened before it gets there. The screen must not 
narrow what
+ * can be loaded, though: these paths load WSDLs from files, archives, the 
classpath
+ * and catalogs, and codegen passes bare relative paths.
+ */
+public class HardenedWSDLLocatorTest extends TestCase {
+
+    private static final String PLAIN_WSDL =
+            "<?xml version='1.0'?>"
+            + "<definitions xmlns='http://schemas.xmlsoap.org/wsdl/'"
+            + " targetNamespace='urn:test'><types/></definitions>";
+
+    private static final String WSDL_WITH_DOCTYPE =
+            "<?xml version='1.0'?>"
+            + "<!DOCTYPE definitions [<!ENTITY xxe SYSTEM 
'file:///etc/passwd'>]>"
+            + "<definitions xmlns='http://schemas.xmlsoap.org/wsdl/'"
+            + " targetNamespace='urn:test'><types>&xxe;</types></definitions>";
+
+    private File written;
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (written != null) {
+            written.delete();
+        }
+    }
+
+    private File write(String content, String suffix) throws Exception {
+        written = File.createTempFile("axis2-hardened-locator", suffix);
+        FileOutputStream out = new FileOutputStream(written);
+        try {
+            out.write(content.getBytes(StandardCharsets.UTF_8));
+        } finally {
+            out.close();
+        }
+        return written;
+    }
+
+    /** A locator already in use keeps resolving; only the content is 
screened. */
+    private WSDLLocator delegateReturning(final String content) {
+        return new WSDLLocator() {
+            public InputSource getBaseInputSource() {
+                return new InputSource(new ByteArrayInputStream(
+                        content.getBytes(StandardCharsets.UTF_8)));
+            }
+
+            public String getBaseURI() {
+                return "urn:delegate";
+            }
+
+            public InputSource getImportInputSource(String parent, String 
location) {
+                return getBaseInputSource();
+            }
+
+            public String getLatestImportURI() {
+                return "urn:delegate-import";
+            }
+
+            public void close() {
+            }
+        };
+    }
+
+    public void testAnOrdinaryWsdlPassesThroughADelegate() {
+        InputSource source = new 
HardenedWSDLLocator(delegateReturning(PLAIN_WSDL))
+                .getBaseInputSource();
+        assertNotNull(source);
+        assertNotNull("wsdl4j must still get a readable stream", 
source.getByteStream());
+    }
+
+    public void testADoctypeFromADelegateIsRefused() {
+        HardenedWSDLLocator locator =
+                new HardenedWSDLLocator(delegateReturning(WSDL_WITH_DOCTYPE));
+        try {
+            locator.getBaseInputSource();
+            fail("a DOCTYPE must not reach wsdl4j");
+        } catch (RuntimeException expected) {
+            assertTrue("should say what was refused, was: " + 
expected.getMessage(),
+                    expected.getMessage().contains("Refusing WSDL document"));
+        }
+    }
+
+    public void testAnImportFromADelegateIsScreenedToo() {
+        HardenedWSDLLocator locator =
+                new HardenedWSDLLocator(delegateReturning(WSDL_WITH_DOCTYPE));
+        try {
+            locator.getImportInputSource("urn:parent", "child.wsdl");
+            fail("imports are fetched by wsdl4j too, so they need the same 
screen");
+        } catch (RuntimeException expected) {
+            assertTrue(expected.getMessage().contains("Refusing WSDL 
document"));
+        }
+    }
+
+    /**
+     * Codegen and the deployment paths hand over bare relative paths, which 
is what
+     * wsdl4j's own readWSDL(String) accepts. Refusing those would have broken 
the
+     * ordinary case rather than the dangerous one -- as it did on the first 
attempt,
+     * caught by the codegen module's own WSDL generation.
+     */
+    public void testARelativeFilePathStillLoads() throws Exception {
+        File wsdl = write(PLAIN_WSDL, ".wsdl");
+        String relative = new File(".").getCanonicalFile().toURI()
+                .relativize(wsdl.getCanonicalFile().toURI()).getPath();
+        // Only meaningful if the temp file really is under the working 
directory;
+        // otherwise fall back to the absolute path, which must also work.
+        String location = new File(relative).isFile() ? relative : 
wsdl.getAbsolutePath();
+
+        InputSource source = new 
HardenedWSDLLocator(location).getBaseInputSource();
+        assertNotNull("a path wsdl4j would accept must still load", source);
+        assertNotNull(source.getByteStream());
+    }
+
+    public void testAFileUrlStillLoads() throws Exception {
+        File wsdl = write(PLAIN_WSDL, ".wsdl");
+        InputSource source =
+                new 
HardenedWSDLLocator(wsdl.toURI().toString()).getBaseInputSource();
+        assertNotNull("file: URLs are how deployment loads WSDLs", source);
+    }
+
+    public void testADoctypeInAFileIsRefused() throws Exception {
+        File wsdl = write(WSDL_WITH_DOCTYPE, ".wsdl");
+        try {
+            new 
HardenedWSDLLocator(wsdl.toURI().toString()).getBaseInputSource();
+            fail("a DOCTYPE in a local WSDL must be refused as well");
+        } catch (RuntimeException expected) {
+            assertTrue(expected.getMessage().contains("Refusing WSDL 
document"));
+        }
+    }
+
+    public void testAMissingLocationSaysSo() {
+        try {
+            new 
HardenedWSDLLocator("no-such-file-anywhere.wsdl").getBaseInputSource();
+            fail("a location that cannot be opened should report that");
+        } catch (RuntimeException expected) {
+            assertTrue("was: " + expected.getMessage(),
+                    expected.getMessage().contains("Failed to load WSDL 
document"));
+        }
+    }
+}
diff --git 
a/modules/metadata/src/org/apache/axis2/jaxws/util/WSDL4JWrapper.java 
b/modules/metadata/src/org/apache/axis2/jaxws/util/WSDL4JWrapper.java
index 24b9cf2fb3..c111aa9a1b 100644
--- a/modules/metadata/src/org/apache/axis2/jaxws/util/WSDL4JWrapper.java
+++ b/modules/metadata/src/org/apache/axis2/jaxws/util/WSDL4JWrapper.java
@@ -19,6 +19,7 @@
 
 package org.apache.axis2.jaxws.util;
 
+import org.apache.axis2.util.HardenedWSDLLocator;
 import org.apache.axis2.Constants;
 import org.apache.axis2.context.ConfigurationContext;
 import org.apache.axis2.description.Parameter;
@@ -708,7 +709,13 @@ public class WSDL4JWrapper implements WSDLWrapper {
                             def = (Definition) 
AccessController.doPrivileged(new PrivilegedExceptionAction() {
                                 public Object run() throws WSDLException {
                                     WSDLReader reader = getWSDLReader();
-                                    return reader.readWSDL(locator);
+                                    // The catalog and module locators keep
+                                    // resolving from a catalog, an archive or 
the
+                                    // classpath; the wrapper only refuses a 
DOCTYPE
+                                    // in what they return, which wsdl4j would
+                                    // otherwise parse with entities enabled.
+                                    return reader.readWSDL(
+                                            new HardenedWSDLLocator(locator));
                                 }
                             });
                         }
@@ -728,7 +735,10 @@ public class WSDL4JWrapper implements WSDLWrapper {
                     def = (Definition) AccessController.doPrivileged(new 
PrivilegedExceptionAction() {
                         public Object run() throws WSDLException {
                             WSDLReader reader = getWSDLReader();
-                            return reader.readWSDL(wsdlExplicitURL);
+                            // No locator here previously, so wsdl4j fetched 
the URL
+                            // and its whole import chain itself, unscreened.
+                            return reader.readWSDL(
+                                    new HardenedWSDLLocator(wsdlExplicitURL));
                         }
                     });
                 }
diff --git a/src/site/markdown/release-notes/2.0.2.md 
b/src/site/markdown/release-notes/2.0.2.md
index 84f2e6751a..0dab023f84 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -115,6 +115,15 @@ in `SECURITY.md`.
   now, as the pattern facet already was. QName enumerations are unaffected: the
   argument split the compiler inserts deliberately is preserved.
 
+- **Every WSDL-loading path is screened, not just the client-side one.** wsdl4j
+  parses with its own unhardened parser and fetches imports itself, so a 
DOCTYPE in
+  a WSDL -- or in anything it imports -- was an XXE in whichever JVM read it. 
Only
+  the client-side path was screened. The JAX-WS loader, the runtime reload 
wrapper,
+  the deployment builder's resolver path and the WSDL2Java entry point now 
screen
+  too, via a locator that refuses a DOCTYPE without changing where documents 
may be
+  loaded from: a delegate locator keeps its own catalog or archive resolution, 
and a
+  bare relative path still works.
+
 - **The schema compiler no longer trusts the schema it is compiling.** 
`XSD2Java`
   and the `axis2-xsd2java-maven-plugin` exist to consume contracts written
   elsewhere, but parsed them with no XXE hardening and dereferenced

Reply via email to