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
