You are viewing a plain text version of this content. The canonical link for it is here.
Posted to java-commits@axis.apache.org by ro...@apache.org on 2026/09/05 14:37:30 UTC
(axis-axis2-java-core) branch master updated (c06f6ef551 -> e170a13bde)
This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
from c06f6ef551 Keep generated output inside the output directory
new 8be5bb5723 Filter the service listing in the model, not only in the view
new 84811c7c87 Say what context deserialization actually enforces, and enforce that
new 583e78e683 Bound multiref resolution against cycles and expansion
new 94c935d140 Bound the MTOM, SwA and SOAP bodies too
new 25db09b07b update same dep on httpcomponents
new 6bdd6a07ea Screen every WSDL-loading path, not just the client-side one
new 00274eedfa Pin the Swagger UI scripts, and stop naming exception classes in faults
new 3ff914a614 Make two threat-model claims true, and scope what they do not cover
new 3cc039d012 Confine packaged-metadata lookup and fix a fail-open schema guard
new 0795c50df1 Stop a remote server retargeting WSDL2Java's own WSDL fetch
new b5a2474b8c Deprecate the mail transport for removal in 2.1.0
new 5e93c08cf1 Drop the unused testkit dependency from transport-h2
new e170a13bde Stop bundling the JMS transport in axis2.war
The 13 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
SECURITY.md | 181 ++++++++++++---
.../axis2/databinding/utils/MultirefHelper.java | 200 ++++++++++++++++-
.../axis2/databinding/utils/MultirefCycleTest.java | 155 +++++++++++++
.../axis2/wsdl/codegen/CodeGenConfiguration.java | 107 ++++++++-
.../axis2/wsdl/codegen/WsdlRedirectTest.java | 120 ++++++++++
.../axis2/jaxws/handler/LogicalMessageImpl.java | 7 +-
.../src/org/apache/axis2/builder/MIMEBuilder.java | 12 +-
.../apache/axis2/builder/RequestSizeLimits.java | 20 ++
.../src/org/apache/axis2/builder/SOAPBuilder.java | 10 +-
.../externalize/ContextDeserializationFilter.java | 153 +++++++++++++
.../context/externalize/SafeObjectInputStream.java | 8 +-
.../deployment/resolver/AARBasedWSDLLocator.java | 9 +-
.../resolver/AARFileBasedURIResolver.java | 9 +-
.../deployment/resolver/WarBasedWSDLLocator.java | 9 +-
.../resolver/WarFileBasedURIResolver.java | 9 +-
.../org/apache/axis2/description/AxisService.java | 20 +-
.../description/WSDL11ToAxisServiceBuilder.java | 14 +-
.../description/WSDLToAxisServiceBuilder.java | 64 ++++--
.../org/apache/axis2/util/HardenedWSDLLocator.java | 245 +++++++++++++++++++++
.../org/apache/axis2/util/MetaInfResources.java | 106 +++++++++
.../kernel/src/org/apache/axis2/util/XMLUtils.java | 5 +-
.../axis2/wsdl/util/WSDLWrapperReloadImpl.java | 9 +-
.../axis2/builder/RequestSizeLimitsTest.java | 32 +++
.../ContextDeserializationFilterTest.java | 178 +++++++++++++++
.../apache/axis2/deployment/URIResolverTest.java | 36 +++
.../description/AbsoluteSchemaLocationTest.java | 110 +++++++++
.../apache/axis2/util/HardenedWSDLLocatorTest.java | 171 ++++++++++++++
.../apache/axis2/util/MetaInfResourcesTest.java | 166 ++++++++++++++
.../description/builder/JAXWSRIWSDLGenerator.java | 4 +-
.../org/apache/axis2/jaxws/util/WSDL4JWrapper.java | 14 +-
.../apache/axis2/openapi/OpenApiConfiguration.java | 24 ++
.../org/apache/axis2/openapi/SwaggerUIHandler.java | 70 +++++-
.../axis2/openapi/SwaggerUIIntegrityTest.java | 106 +++++++++
.../src/org/apache/axis2/saaj/SOAPBodyImpl.java | 6 +-
.../src/org/apache/axis2/saaj/util/SAAJUtil.java | 6 +-
.../src/userguide/springbootdemo-wildfly/pom.xml | 2 +-
modules/transport-h2/pom.xml | 6 -
.../apache/axis2/transport/http/AxisServlet.java | 28 ++-
.../axis2/transport/http/HTTPTransportUtils.java | 54 +----
.../apache/axis2/transport/http/ListingAgent.java | 13 +-
.../transport/http/ServiceListingFilterTest.java | 111 ++++++++++
modules/webapp/pom.xml | 5 -
modules/webapp/scripts/build.xml | 6 +
src/site/markdown/release-notes/2.0.2.md | 137 +++++++++++-
44 files changed, 2605 insertions(+), 152 deletions(-)
create mode 100644 modules/adb/test/org/apache/axis2/databinding/utils/MultirefCycleTest.java
create mode 100644 modules/codegen/test/org/apache/axis2/wsdl/codegen/WsdlRedirectTest.java
create mode 100644 modules/kernel/src/org/apache/axis2/context/externalize/ContextDeserializationFilter.java
create mode 100644 modules/kernel/src/org/apache/axis2/util/HardenedWSDLLocator.java
create mode 100644 modules/kernel/src/org/apache/axis2/util/MetaInfResources.java
create mode 100644 modules/kernel/test/org/apache/axis2/context/externalize/ContextDeserializationFilterTest.java
create mode 100644 modules/kernel/test/org/apache/axis2/description/AbsoluteSchemaLocationTest.java
create mode 100644 modules/kernel/test/org/apache/axis2/util/HardenedWSDLLocatorTest.java
create mode 100644 modules/kernel/test/org/apache/axis2/util/MetaInfResourcesTest.java
create mode 100644 modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIIntegrityTest.java
create mode 100644 modules/transport/http/src/test/java/org/apache/axis2/transport/http/ServiceListingFilterTest.java
(axis-axis2-java-core) 04/13: Bound the MTOM, SwA and SOAP bodies too
Posted by ro...@apache.org.
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 94c935d1406bc0875daebd36994595efa2f48ae0
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 11:23:55 2026 -1000
Bound the MTOM, SwA and SOAP bodies too
The ceilings added for the form builders left the other stream-reading builders
unbounded, which bounds nothing: the caller picks the builder by choosing the
Content-Type, so sending multipart/related avoided them entirely. Bound the
whole multipart body in MIMEBuilder, before the parts are buffered to heap or
disk, and a plain SOAP or POX body in SOAPBuilder. Its MIME path takes its root
part from the now-bounded multipart body and needs no ceiling of its own. Item
10 of the threat model scoped itself to the form builders, which is what let
this stand; it now says every stream-reading builder.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
SECURITY.md | 18 ++++++++----
.../src/org/apache/axis2/builder/MIMEBuilder.java | 12 ++++++--
.../apache/axis2/builder/RequestSizeLimits.java | 20 ++++++++++++++
.../src/org/apache/axis2/builder/SOAPBuilder.java | 10 ++++++-
.../axis2/builder/RequestSizeLimitsTest.java | 32 ++++++++++++++++++++++
src/site/markdown/release-notes/2.0.2.md | 10 +++++--
6 files changed, 90 insertions(+), 12 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 76475cfb4f..1feeae5d1b 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -312,12 +312,18 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
does not currently support. Operators in cloud environments should pair
these settings with network egress controls.
-10. **Request body ceilings (2.0.2):** The `multipart/form-data` and
- `application/x-www-form-urlencoded` builders read the transport stream
- directly, so a servlet container's post-size limit never sees the body.
- `multipartMaxRequestSize` and `multipartMaxFileSize` (100 MB) and
- `formUrlEncodedMaxRequestSize` (2 MB) bound them; `-1` restores the
- previous unbounded behaviour, and either may be set per service.
+10. **Request body ceilings (2.0.2):** The message builders read the transport
+ stream directly, so a servlet container's post-size limit never sees the
+ body. `multipartMaxRequestSize` and `multipartMaxFileSize` (100 MB),
+ `formUrlEncodedMaxRequestSize` (2 MB), `mtomMaxRequestSize` (100 MB, the
+ whole `multipart/related` body, so MTOM and SwA) and `soapMaxRequestSize`
+ (100 MB, a plain SOAP or POX body) bound them; `-1` restores the previous
+ unbounded behaviour, and any may be set per service.
+
+ Every builder that reads the stream has to be bounded, not just the ones
+ whose limits were reported: the caller picks which builder runs by choosing
+ the Content-Type, so a ceiling on the form builders alone is avoided by
+ sending `multipart/related` instead.
Multipart temp files are now deleted rather than accumulating: form-field
parts as soon as their text is read, file parts once the item backing the
`DataHandler` is unreachable.
diff --git a/modules/kernel/src/org/apache/axis2/builder/MIMEBuilder.java b/modules/kernel/src/org/apache/axis2/builder/MIMEBuilder.java
index 454bda3cc0..0e1f61e811 100644
--- a/modules/kernel/src/org/apache/axis2/builder/MIMEBuilder.java
+++ b/modules/kernel/src/org/apache/axis2/builder/MIMEBuilder.java
@@ -36,8 +36,16 @@ public class MIMEBuilder implements Builder {
public OMElement processDocument(InputStream inputStream, String contentType,
MessageContext msgContext)
throws AxisFault {
- Attachments attachments =
- BuilderUtil.createAttachmentsMap(msgContext, inputStream, contentType);
+ // Bounded before the parts are read: the attachment map buffers parts in
+ // heap by default, or on disk with cacheAttachments, and neither was
+ // bounded. The caller chooses this builder by sending
+ // multipart/related, so leaving it unbounded left the form-builder
+ // ceilings trivially avoidable.
+ long maxRequestSize = RequestSizeLimits.resolve(msgContext,
+ RequestSizeLimits.MTOM_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_MTOM_MAX_REQUEST_SIZE);
+ Attachments attachments = BuilderUtil.createAttachmentsMap(msgContext,
+ BoundedInputStream.wrap(inputStream, maxRequestSize), contentType);
ContentType ct;
try {
diff --git a/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java b/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
index 28b48682a3..ce9461b37d 100644
--- a/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
+++ b/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
@@ -62,6 +62,26 @@ public final class RequestSizeLimits {
public static final long DEFAULT_FORM_URLENCODED_MAX_REQUEST_SIZE = 2L * 1024 * 1024;
/** Sentinel for "no ceiling", matching the commons-fileupload2 convention. */
+ /**
+ * Ceiling on a {@code multipart/related} body: MTOM and SwA.
+ * <p>
+ * The same reasoning as the form builders, which is why this belongs here: the
+ * MIME builder reads the transport stream directly, so a servlet container's
+ * post-size limit never sees the body. It also means the caller picks which
+ * builder runs by choosing the Content-Type, so bounding only the form builders
+ * bounds nothing -- an attacker simply sends {@code multipart/related}.
+ */
+ public static final String MTOM_MAX_REQUEST_SIZE = "mtomMaxRequestSize";
+
+ /** Ceiling on a plain SOAP or POX body. */
+ public static final String SOAP_MAX_REQUEST_SIZE = "soapMaxRequestSize";
+
+ /** Default {@link #MTOM_MAX_REQUEST_SIZE}: 100 MB. */
+ public static final long DEFAULT_MTOM_MAX_REQUEST_SIZE = 100L * 1024 * 1024;
+
+ /** Default {@link #SOAP_MAX_REQUEST_SIZE}: 100 MB. */
+ public static final long DEFAULT_SOAP_MAX_REQUEST_SIZE = 100L * 1024 * 1024;
+
public static final long UNLIMITED = -1L;
private RequestSizeLimits() {
diff --git a/modules/kernel/src/org/apache/axis2/builder/SOAPBuilder.java b/modules/kernel/src/org/apache/axis2/builder/SOAPBuilder.java
index 7c4167f992..c16c43032b 100644
--- a/modules/kernel/src/org/apache/axis2/builder/SOAPBuilder.java
+++ b/modules/kernel/src/org/apache/axis2/builder/SOAPBuilder.java
@@ -41,7 +41,15 @@ public class SOAPBuilder implements MIMEAwareBuilder {
// createSOAPModelBuilder takes care of configuring the underlying parser to
// avoid the security issue described in CVE-2010-1632
- OMXMLParserWrapper builder = OMXMLBuilderFactory.createSOAPModelBuilder(inputStream,
+ // Bounded for the same reason as the other builders: this reads the
+ // transport stream directly. processMIMEMessage below takes its root
+ // part from an already-bounded multipart body, so it needs no ceiling
+ // of its own.
+ long maxRequestSize = RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.SOAP_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_SOAP_MAX_REQUEST_SIZE);
+ OMXMLParserWrapper builder = OMXMLBuilderFactory.createSOAPModelBuilder(
+ BoundedInputStream.wrap(inputStream, maxRequestSize),
charSetEncoding);
messageContext.setProperty(Constants.BUILDER, builder);
SOAPEnvelope envelope = (SOAPEnvelope) builder.getDocumentElement();
diff --git a/modules/kernel/test/org/apache/axis2/builder/RequestSizeLimitsTest.java b/modules/kernel/test/org/apache/axis2/builder/RequestSizeLimitsTest.java
index 20be388192..6ce056919a 100644
--- a/modules/kernel/test/org/apache/axis2/builder/RequestSizeLimitsTest.java
+++ b/modules/kernel/test/org/apache/axis2/builder/RequestSizeLimitsTest.java
@@ -127,4 +127,36 @@ public class RequestSizeLimitsTest extends TestCase {
}
return total;
}
+
+ /**
+ * The ceilings the form builders had were avoidable: the caller picks the
+ * builder by choosing the Content-Type, so multipart/related and a plain SOAP
+ * body needed their own.
+ */
+ public void testMtomAndSoapCeilingsHaveDefaults() throws Exception {
+ assertEquals(RequestSizeLimits.DEFAULT_MTOM_MAX_REQUEST_SIZE,
+ RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.MTOM_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_MTOM_MAX_REQUEST_SIZE));
+ assertEquals(RequestSizeLimits.DEFAULT_SOAP_MAX_REQUEST_SIZE,
+ RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.SOAP_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_SOAP_MAX_REQUEST_SIZE));
+ }
+
+ public void testMtomCeilingIsConfigurable() throws Exception {
+ axisConfiguration.addParameter(
+ new Parameter(RequestSizeLimits.MTOM_MAX_REQUEST_SIZE, "8192"));
+ assertEquals(8192L, RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.MTOM_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_MTOM_MAX_REQUEST_SIZE));
+ }
+
+ public void testSoapCeilingCanBeOptedOut() throws Exception {
+ axisConfiguration.addParameter(
+ new Parameter(RequestSizeLimits.SOAP_MAX_REQUEST_SIZE, "-1"));
+ assertEquals(RequestSizeLimits.UNLIMITED, RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.SOAP_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_SOAP_MAX_REQUEST_SIZE));
+ }
}
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index 03703de9a0..84f2e6751a 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -69,9 +69,13 @@ in `SECURITY.md`.
- **Request bodies are bounded.** The `multipart/form-data` and
`application/x-www-form-urlencoded` builders read the transport stream directly,
so a servlet container's post-size limit never saw the body.
- `multipartMaxRequestSize` and `multipartMaxFileSize` (100 MB) and
- `formUrlEncodedMaxRequestSize` (2 MB) now bound them; `-1` restores the previous
- unbounded behaviour, and either may be set per service. The ceilings are enforced
+ `multipartMaxRequestSize` and `multipartMaxFileSize` (100 MB),
+ `formUrlEncodedMaxRequestSize` (2 MB), `mtomMaxRequestSize` (100 MB, covering the
+ whole `multipart/related` body and so MTOM and SwA) and `soapMaxRequestSize`
+ (100 MB, a plain SOAP or POX body) now bound them; `-1` restores the previous
+ unbounded behaviour, and any may be set per service. All the stream-reading
+ builders are covered, not only the form ones: the caller picks the builder by
+ choosing the Content-Type, so bounding some of them bounds none. The ceilings are enforced
against bytes actually read, so a chunked body is bounded on the same terms as a
declared one. Multipart temporary files are deleted rather than accumulating.
(axis-axis2-java-core) 10/13: Stop a remote server retargeting WSDL2Java's own WSDL fetch
Posted by ro...@apache.org.
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 <ro...@gmail.com>
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) <no...@anthropic.com>
---
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
(axis-axis2-java-core) 06/13: Screen every WSDL-loading path, not just the client-side one
Posted by ro...@apache.org.
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 <ro...@gmail.com>
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) <no...@anthropic.com>
---
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
(axis-axis2-java-core) 13/13: Stop bundling the JMS transport in axis2.war
Posted by ro...@apache.org.
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 e170a13bdedd92880db2b2d6fdac5f67665840ff
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sat Sep 5 04:32:25 2026 -1000
Stop bundling the JMS transport in axis2.war
No default axis2.xml enables a JMS listener or sender, yet every WAR carried the
transport and its JNDI lookup code. It was not usable as shipped in any case:
jakarta.jms-api is an API with no provider, so a WAR wanting JMS always had to
add a broker client. The module is unchanged and still ships in the binary
distribution's lib/, so a standalone server is unaffected; the distribution
builds its WAR from that directory, which is why the exclude belongs there too.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
modules/webapp/pom.xml | 5 -----
modules/webapp/scripts/build.xml | 6 ++++++
src/site/markdown/release-notes/2.0.2.md | 12 ++++++++++++
3 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/modules/webapp/pom.xml b/modules/webapp/pom.xml
index 8af616c58c..13d950be02 100644
--- a/modules/webapp/pom.xml
+++ b/modules/webapp/pom.xml
@@ -137,11 +137,6 @@
<artifactId>axis2-transport-local</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>axis2-transport-jms</artifactId>
- <version>${project.version}</version>
- </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>axis2-transport-mail</artifactId>
diff --git a/modules/webapp/scripts/build.xml b/modules/webapp/scripts/build.xml
index 885154a59d..8e968f27a5 100644
--- a/modules/webapp/scripts/build.xml
+++ b/modules/webapp/scripts/build.xml
@@ -78,6 +78,12 @@
<exclude name="axis2-java2wsdl-*.jar"/>
<!-- codegen is required for Rampart because of AXIS2-4265 -->
<!-- exclude name="axis2-codegen*.jar"/ -->
+ <!-- The JMS transport is shipped in lib/ but not bundled in the WAR:
+ no default axis2.xml enables a JMS listener or sender, and
+ jakarta.jms-api is an API with no provider, so a WAR that wanted
+ JMS always had to add a broker client anyway. -->
+ <exclude name="axis2-transport-jms-*.jar" />
+ <exclude name="jakarta.jms-api-*.jar" />
<exclude name="commons-cli-*.jar" />
<exclude name="jibx-bind-*.jar" />
<exclude name="bcel-*.jar" />
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index f9b540e895..e106031b2a 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -3,6 +3,18 @@ Apache Axis2 2.0.2 Release Notes
(Unreleased)
+## Distribution Changes
+
+- **The JMS transport is no longer bundled in `axis2.war`.** `axis2-transport-jms`
+ and `jakarta.jms-api` shipped in every WAR while no default `axis2.xml` enables a
+ JMS listener or sender, so every deployment carried the transport -- and its JNDI
+ lookup code -- without asking for it. It was never usable as shipped in any case:
+ `jakarta.jms-api` is an API with no provider, so enabling JMS always required
+ adding a broker client. The module is unchanged, remains a supported transport,
+ and is still in `lib/` of the binary distribution, so a standalone server is
+ unaffected. A WAR deployment that uses JMS now adds `axis2-transport-jms` to
+ `WEB-INF/lib` alongside the broker client it already had to add.
+
## Deprecated for Removal
- **The mail transport (`axis2-transport-mail`) is deprecated and is planned for
(axis-axis2-java-core) 03/13: Bound multiref resolution against cycles and expansion
Posted by ro...@apache.org.
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 583e78e68341f50735d27a23c58b197a04737535
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 11:04:39 2026 -1000
Bound multiref resolution against cycles and expansion
Resolution had no cycle check and no budget, on the anonymous RPC/POJO path: a
reference leading back on itself recursed until the stack ended, and a graph
with no cycle could still double per level and end in OutOfMemoryError. Element
cycles are refused, nesting and total work are bounded, and copying a referenced
element is charged before the copy, since at depth it is the copy that exhausts
the heap and only direct children are moved, so counting those stays flat while
the subtrees double. The bean path is bounded by depth rather than refused: a
self-referencing object graph is legitimate SOAP encoding, which MultirefTest
sends, and rejecting re-entry there broke it.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../axis2/databinding/utils/MultirefHelper.java | 200 ++++++++++++++++++++-
.../axis2/databinding/utils/MultirefCycleTest.java | 155 ++++++++++++++++
src/site/markdown/release-notes/2.0.2.md | 12 ++
3 files changed, 362 insertions(+), 5 deletions(-)
diff --git a/modules/adb/src/org/apache/axis2/databinding/utils/MultirefHelper.java b/modules/adb/src/org/apache/axis2/databinding/utils/MultirefHelper.java
index 5e30c20543..862b97b655 100644
--- a/modules/adb/src/org/apache/axis2/databinding/utils/MultirefHelper.java
+++ b/modules/adb/src/org/apache/axis2/databinding/utils/MultirefHelper.java
@@ -52,6 +52,142 @@ public class MultirefHelper {
private HashMap elementMap = new HashMap();
private HashMap omElementMap = new HashMap();
+ /**
+ * Maximum nesting of multiref resolutions, and maximum number of resolutions
+ * per message. Both -1 for unbounded.
+ * <p>
+ * A reference is resolved by deep-cloning the referenced element, and both the
+ * element and the bean paths memoise a resolution only <em>after</em> it
+ * returns, so a reference that leads back to itself recurses until the stack
+ * ends. Cycles are refused outright by tracking what is being resolved; these
+ * bound the other shape, a reference graph with no cycle in it whose expansion
+ * still doubles at every level -- the multiref analogue of an entity-expansion
+ * bomb, on well-formed XML that no parser limit sees.
+ */
+ private static final int MAX_DEPTH =
+ getIntProperty("org.apache.axis2.databinding.multiref.maxDepth", 64);
+ private static final int MAX_RESOLUTIONS =
+ getIntProperty("org.apache.axis2.databinding.multiref.maxResolutions", 5000);
+
+ /**
+ * Maximum nodes a message may bring into existence by expanding references.
+ * <p>
+ * This is the limit that matters for the doubling shape. Resolutions stay linear
+ * because each id is memoised after the first one, so counting them catches
+ * nothing: what grows is the <em>size</em> of each resolved element, since every
+ * level inlines two copies of the level below. Metering nodes as they are moved
+ * in stops it while the trees are still small, before a clone of the next level
+ * up would exhaust the heap.
+ */
+ private static final int MAX_EXPANDED_NODES =
+ getIntProperty("org.apache.axis2.databinding.multiref.maxExpandedNodes", 50000);
+
+ /**
+ * Element ids currently being resolved, so a reference back into one is a cycle.
+ * <p>
+ * Only the element path uses this. A cyclic reference there means inlining XML
+ * into itself, which cannot terminate. The bean path is different: SOAP encoding
+ * allows a cyclic <em>object</em> graph -- an employee who is their own employer
+ * is a documented multiref shape, exercised by MultirefTest -- so that path is
+ * bounded by nesting depth rather than refused outright.
+ */
+ private final java.util.Set resolvingElements = new java.util.HashSet();
+
+ /** Current nesting of resolutions on either path, against MAX_DEPTH. */
+ private int nesting;
+
+ /** Resolutions performed for this message, against MAX_RESOLUTIONS. */
+ private int resolutions;
+
+ /** Nodes moved in by expansion, against MAX_EXPANDED_NODES. */
+ private int expandedNodes;
+
+ /**
+ * Charges the cost of copying an element, before the copy is made.
+ * <p>
+ * It has to be counted before rather than after: at depth the element being
+ * copied is already large, and it is the copy that exhausts the heap. Counting
+ * the nodes actually moved does not work either -- only the direct children are
+ * moved, each carrying a whole subtree with it, so the count stays flat while the
+ * subtrees double.
+ */
+ private void chargeCopyOf(OMElement element) throws AxisFault {
+ if (MAX_EXPANDED_NODES < 0) {
+ return;
+ }
+ int remaining = MAX_EXPANDED_NODES - expandedNodes;
+ int size = countNodes(element, remaining + 1);
+ expandedNodes += size;
+ if (expandedNodes > MAX_EXPANDED_NODES) {
+ throw new AxisFault("Expanding multiref references in this message would"
+ + " copy more than " + MAX_EXPANDED_NODES + " nodes");
+ }
+ }
+
+ /**
+ * Counts nodes, stopping once the limit is passed so that measuring a large tree
+ * is not itself the expensive part.
+ */
+ private static int countNodes(OMElement element, int limit) {
+ int count = 1;
+ Iterator children = element.getChildElements();
+ while (children.hasNext() && count <= limit) {
+ count += countNodes((OMElement) children.next(), limit - count);
+ }
+ return count;
+ }
+
+ private static int getIntProperty(String name, int defaultValue) {
+ try {
+ String value = System.getProperty(name);
+ if (value != null && !value.trim().isEmpty()) {
+ return Integer.parseInt(value.trim());
+ }
+ } catch (RuntimeException e) {
+ // Unreadable or unparseable: keep the default rather than run unbounded.
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Claims an id for resolution, refusing a cycle or an over-budget message.
+ * Every caller must {@link #release} in a finally block.
+ */
+ /** Enters a resolution on either path, bounding nesting and total work. */
+ private void enter() throws AxisFault {
+ if (MAX_DEPTH >= 0 && ++nesting > MAX_DEPTH) {
+ nesting--;
+ throw new AxisFault("Multiref references nested deeper than " + MAX_DEPTH);
+ }
+ if (MAX_RESOLUTIONS >= 0 && ++resolutions > MAX_RESOLUTIONS) {
+ nesting--;
+ throw new AxisFault("Message resolves more than " + MAX_RESOLUTIONS
+ + " multiref references");
+ }
+ }
+
+ private void exit() {
+ nesting--;
+ }
+
+ /** Enters an element resolution, where a reference back into one is a cycle. */
+ private void claimElement(String id) throws AxisFault {
+ if (!resolvingElements.add(id)) {
+ throw new AxisFault("Cyclic multiref reference: " + id);
+ }
+ try {
+ enter();
+ } catch (AxisFault fault) {
+ resolvingElements.remove(id);
+ throw fault;
+ }
+ }
+
+ private void releaseElement(String id) {
+ resolvingElements.remove(id);
+ exit();
+ }
+
public MultirefHelper(OMElement parent) {
this.parent = parent;
}
@@ -72,10 +208,17 @@ public class MultirefHelper {
if (val == null) {
throw new AxisFault("Invalid reference :" + id);
} else {
- OMElement ele = processElementforRefs(val);
- OMElement cloneele = elementClone(ele);
- omElementMap.put(id, cloneele);
- return cloneele;
+ // The memo below is written only once this returns, so without the
+ // claim a reference leading back to id would recurse into itself.
+ claimElement(id);
+ try {
+ OMElement ele = processElementforRefs(val);
+ OMElement cloneele = elementClone(ele);
+ omElementMap.put(id, cloneele);
+ return cloneele;
+ } finally {
+ releaseElement(id);
+ }
}
}
@@ -90,6 +233,7 @@ public class MultirefHelper {
if (tempele == null) {
tempele = processOMElementRef(ref);
}
+ chargeCopyOf(tempele);
OMElement ele2 = elementClone(tempele);
Iterator itrChild = ele2.getChildren();
while (itrChild.hasNext()) {
@@ -121,6 +265,13 @@ public class MultirefHelper {
if (val == null) {
throw new AxisFault("Invalid reference :" + id);
} else {
+ // Not cycle-refused: objectmap is populated only after
+ // BeanUtil.deserialize returns, so a self-referencing object graph
+ // re-enters here legitimately (MultirefTest.testechoEmployee sends
+ // exactly that). Nesting depth is what keeps a crafted one from
+ // exhausting the stack.
+ enter();
+ try {
if (SimpleTypeMapper.isSimpleType(javatype)) {
/**
* in this case OM element can not contains more child, that is no way to get
@@ -153,6 +304,9 @@ public class MultirefHelper {
objectmap.put(id, obj);
return obj;
}
+ } finally {
+ exit();
+ }
}
}
@@ -235,10 +389,42 @@ public class MultirefHelper {
}
+ /**
+ * Bounds one message's worth of href expansion.
+ * <p>
+ * Each resolution copies the referenced element's children into the element being
+ * processed and the walk then descends into them, so a reference graph that leads
+ * back on itself expands without end. Depth alone is not enough: a graph with no
+ * cycle can still double at every level.
+ */
+ private static final class HrefBudget {
+ private int expansions;
+
+ void spend(int depth) throws AxisFault {
+ if (MAX_DEPTH >= 0 && depth > MAX_DEPTH) {
+ throw new AxisFault("href references nested deeper than " + MAX_DEPTH);
+ }
+ if (MAX_RESOLUTIONS >= 0 && ++expansions > MAX_RESOLUTIONS) {
+ throw new AxisFault("Message expands more than " + MAX_RESOLUTIONS
+ + " href references");
+ }
+ }
+ }
+
public static void processHrefAttributes(Map idAndOMElementMap,
OMElement elementToProcess,
OMFactory omFactory)
throws AxisFault {
+ processHrefAttributes(idAndOMElementMap, elementToProcess, omFactory,
+ new HrefBudget(), 0);
+ }
+
+ private static void processHrefAttributes(Map idAndOMElementMap,
+ OMElement elementToProcess,
+ OMFactory omFactory,
+ HrefBudget budget,
+ int depth)
+ throws AxisFault {
// first check whether this element has an href value.
// href is also an unqualifed attribute
@@ -258,6 +444,9 @@ public class MultirefHelper {
// now we have to remove the hrefAttribute and add all the child elements to the
// element being proccesed
elementToProcess.removeAttribute(hrefAttribute);
+ // Charged before the copy: the children added here are walked
+ // below, and may carry hrefs of their own.
+ budget.spend(depth);
OMElement clonedReferenceElement = getClonedOMElement(referedOMElement, omFactory);
OMNode omNode = null;
for (Iterator iter = clonedReferenceElement.getChildren(); iter.hasNext();) {
@@ -283,7 +472,8 @@ public class MultirefHelper {
OMElement childOMElement = null;
for (Iterator iter = elementToProcess.getChildElements(); iter.hasNext();) {
childOMElement = (OMElement) iter.next();
- processHrefAttributes(idAndOMElementMap, childOMElement, omFactory);
+ processHrefAttributes(idAndOMElementMap, childOMElement, omFactory,
+ budget, depth + 1);
}
}
diff --git a/modules/adb/test/org/apache/axis2/databinding/utils/MultirefCycleTest.java b/modules/adb/test/org/apache/axis2/databinding/utils/MultirefCycleTest.java
new file mode 100644
index 0000000000..7790a8fbb2
--- /dev/null
+++ b/modules/adb/test/org/apache/axis2/databinding/utils/MultirefCycleTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.databinding.utils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import junit.framework.TestCase;
+
+import org.apache.axiom.om.OMAbstractFactory;
+import org.apache.axiom.om.OMElement;
+import org.apache.axiom.om.OMXMLBuilderFactory;
+import org.apache.axiom.soap.SOAPEnvelope;
+import org.apache.axis2.AxisFault;
+
+/**
+ * Multiref resolution deep-clones the referenced element and memoises the result
+ * only after resolving finishes, so a reference leading back on itself recurses
+ * until the stack ends. A reference graph with no cycle can still double at every
+ * level, which is the same shape as an entity-expansion bomb on XML no parser limit
+ * objects to. Both arrive on the anonymous RPC/POJO receiver path.
+ */
+public class MultirefCycleTest extends TestCase {
+
+ private OMElement parse(String xml) {
+ return OMXMLBuilderFactory.createOMBuilder(
+ new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))
+ .getDocumentElement();
+ }
+
+ /** A refers to B, B back to A. Previously a StackOverflowError. */
+ public void testASelfReferencingCycleIsRefused() throws Exception {
+ OMElement body = parse(
+ "<body>"
+ + " <multiref id='a'><next href='#b'/></multiref>"
+ + " <multiref id='b'><next href='#a'/></multiref>"
+ + "</body>");
+ MultirefHelper helper = new MultirefHelper(body);
+ try {
+ helper.processOMElementRef("a");
+ fail("a cyclic multiref must be refused, not recursed");
+ } catch (AxisFault expected) {
+ assertTrue("should name the cycle, was: " + expected.getMessage(),
+ expected.getMessage().contains("Cyclic multiref"));
+ }
+ }
+
+ /** A reference straight back to its own id. */
+ public void testADirectSelfReferenceIsRefused() throws Exception {
+ OMElement body = parse(
+ "<body><multiref id='a'><next href='#a'/></multiref></body>");
+ MultirefHelper helper = new MultirefHelper(body);
+ try {
+ helper.processOMElementRef("a");
+ fail("a self-reference must be refused");
+ } catch (AxisFault expected) {
+ assertTrue(expected.getMessage().contains("Cyclic multiref"));
+ }
+ }
+
+ /** An ordinary chain still resolves: the guard must not break multiref. */
+ public void testAnAcyclicChainStillResolves() throws Exception {
+ OMElement body = parse(
+ "<body>"
+ + " <multiref id='a'><next href='#b'/></multiref>"
+ + " <multiref id='b'><leaf>value</leaf></multiref>"
+ + "</body>");
+ OMElement resolved = new MultirefHelper(body).processOMElementRef("a");
+ assertNotNull(resolved);
+ assertTrue("the referenced content should have been inlined",
+ resolved.toString().contains("value"));
+ }
+
+ /**
+ * The doubling shape: no cycle anywhere, so cycle detection alone would let it
+ * through. Each level references the next twice, so expansion is exponential in
+ * the depth and the budget is what stops it.
+ */
+ public void testADoublingReferenceGraphIsRefused() throws Exception {
+ StringBuilder xml = new StringBuilder("<body>");
+ int levels = 40;
+ for (int i = 0; i < levels; i++) {
+ xml.append("<multiref id='n").append(i).append("'>")
+ .append("<a href='#n").append(i + 1).append("'/>")
+ .append("<b href='#n").append(i + 1).append("'/>")
+ .append("</multiref>");
+ }
+ xml.append("<multiref id='n").append(levels).append("'><leaf>x</leaf></multiref>");
+ xml.append("</body>");
+
+ MultirefHelper helper = new MultirefHelper(parse(xml.toString()));
+ try {
+ helper.processOMElementRef("n0");
+ fail("an exponentially expanding reference graph must be refused");
+ } catch (AxisFault expected) {
+ assertTrue("should name the budget, was: " + expected.getMessage(),
+ expected.getMessage().contains("multiref references"));
+ }
+ }
+
+ /** The static href path expands in place and then walks what it added. */
+ public void testTheStaticHrefPathRefusesACycle() throws Exception {
+ String envelope =
+ "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'>"
+ + "<soapenv:Body>"
+ + " <op><arg href='#a'/></op>"
+ + " <multiref id='a'><next href='#a'/></multiref>"
+ + "</soapenv:Body></soapenv:Envelope>";
+ SOAPEnvelope soapEnvelope = (SOAPEnvelope) OMXMLBuilderFactory.createSOAPModelBuilder(
+ new ByteArrayInputStream(envelope.getBytes(StandardCharsets.UTF_8)), null)
+ .getDocumentElement();
+ try {
+ MultirefHelper.processHrefAttributes(soapEnvelope);
+ fail("unbounded href expansion must be refused");
+ } catch (AxisFault expected) {
+ assertTrue("should name depth or the budget, was: " + expected.getMessage(),
+ expected.getMessage().contains("href references"));
+ } catch (StackOverflowError e) {
+ fail("expansion should be refused by budget, not end in a stack overflow");
+ }
+ }
+
+ /** Keeps the factory import honest and the ordinary static path working. */
+ public void testTheStaticHrefPathStillResolvesAnOrdinaryReference() throws Exception {
+ String envelope =
+ "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'>"
+ + "<soapenv:Body>"
+ + " <op><arg href='#a'/></op>"
+ + " <multiref id='a'><name>real</name></multiref>"
+ + "</soapenv:Body></soapenv:Envelope>";
+ SOAPEnvelope soapEnvelope = (SOAPEnvelope) OMXMLBuilderFactory.createSOAPModelBuilder(
+ new ByteArrayInputStream(envelope.getBytes(StandardCharsets.UTF_8)), null)
+ .getDocumentElement();
+ MultirefHelper.processHrefAttributes(soapEnvelope);
+ assertTrue("the reference should have been inlined",
+ soapEnvelope.getBody().toString().contains("real"));
+ assertNotNull(OMAbstractFactory.getOMFactory());
+ }
+}
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index b1149f214a..03703de9a0 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -54,6 +54,18 @@ in `SECURITY.md`.
so refused ordinary destinations whose names contained one: a queue called
`alarming` contains "RMI", `dns-events` contains "DNS". Such names now work again.
+- **Multiref resolution is bounded.** SOAP multiref references were resolved with no
+ cycle check and no expansion budget, on the anonymous RPC/POJO receiver path. A
+ reference leading back on itself recursed until the stack ended, and a reference
+ graph with no cycle at all could still double at every level -- an entity-expansion
+ bomb built from well-formed XML that no parser limit objects to, ending in
+ `OutOfMemoryError`. Cyclic *element* references are now refused, nesting depth and
+ total work are bounded, and the cost of copying a referenced element is charged
+ before the copy is made. A cyclic *object* graph, which SOAP encoding permits and
+ Axis2 has always accepted, still deserializes. The limits are
+ `org.apache.axis2.databinding.multiref.maxDepth` (64),
+ `maxResolutions` (5000) and `maxExpandedNodes` (50000); `-1` for unbounded.
+
- **Request bodies are bounded.** The `multipart/form-data` and
`application/x-www-form-urlencoded` builders read the transport stream directly,
so a servlet container's post-size limit never saw the body.
(axis-axis2-java-core) 12/13: Drop the unused testkit dependency from transport-h2
Posted by ro...@apache.org.
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 5e93c08cf1ba1fab92f3f91f71ef400761d0dfca
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sat Sep 5 03:58:18 2026 -1000
Drop the unused testkit dependency from transport-h2
No source in the module references axis2-transport-testkit; the declaration
was carried over from the transports that do use it. All 84 tests still run.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
modules/transport-h2/pom.xml | 6 ------
1 file changed, 6 deletions(-)
diff --git a/modules/transport-h2/pom.xml b/modules/transport-h2/pom.xml
index a8cf1d7745..73202518a8 100644
--- a/modules/transport-h2/pom.xml
+++ b/modules/transport-h2/pom.xml
@@ -94,12 +94,6 @@
<artifactId>xmlunit-legacy</artifactId>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>org.apache.axis2</groupId>
- <artifactId>axis2-transport-testkit</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
<dependency>
<groupId>org.apache.ws.commons.axiom</groupId>
<artifactId>axiom-truth</artifactId>
(axis-axis2-java-core) 05/13: update same dep on httpcomponents
Posted by ro...@apache.org.
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 25db09b07bc206049717e52126009af2a7dfe597
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 11:51:51 2026 -1000
update same dep on httpcomponents
---
modules/samples/userguide/src/userguide/springbootdemo-wildfly/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/modules/samples/userguide/src/userguide/springbootdemo-wildfly/pom.xml b/modules/samples/userguide/src/userguide/springbootdemo-wildfly/pom.xml
index 87215251f3..a476996372 100644
--- a/modules/samples/userguide/src/userguide/springbootdemo-wildfly/pom.xml
+++ b/modules/samples/userguide/src/userguide/springbootdemo-wildfly/pom.xml
@@ -199,7 +199,7 @@
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
- <version>5.4.1</version>
+ <version>5.4.3</version>
</dependency>
<dependency>
<groupId>org.apache.axis2</groupId>
(axis-axis2-java-core) 09/13: Confine packaged-metadata lookup and fix a fail-open schema guard
Posted by ro...@apache.org.
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 3cc039d01296910585086f0471fd9250c3148d71
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sat Sep 5 03:22:45 2026 -1000
Confine packaged-metadata lookup and fix a fail-open schema guard
The ?xsd= and ?wsdl2= queries resolved a requested name with
getResourceAsStream, which delegates parent-first, so they served META-INF
schemas from any jar on the classpath while the file routes -- fixed for this
once as AXIS2-5846 -- did not. Both rules now live in one kernel helper,
MetaInfResources, so the two routes cannot drift apart again. Separately, the
default schema resolver classified schemaLocation with java.net.URI and ignored
URISyntaxException, so a location URI rejects but URL accepts was resolved as
relative and fetched; classification is textual now.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
SECURITY.md | 50 ++++++-
.../org/apache/axis2/description/AxisService.java | 20 +--
.../description/WSDLToAxisServiceBuilder.java | 64 ++++++--
.../org/apache/axis2/util/MetaInfResources.java | 106 +++++++++++++
.../description/AbsoluteSchemaLocationTest.java | 110 ++++++++++++++
.../apache/axis2/util/MetaInfResourcesTest.java | 166 +++++++++++++++++++++
.../axis2/transport/http/HTTPTransportUtils.java | 54 +------
src/site/markdown/release-notes/2.0.2.md | 24 +++
8 files changed, 523 insertions(+), 71 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 41c3f93a79..e529a9b420 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -264,6 +264,19 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
`DefaultURIResolver`; a location that is not absolute is looked up inside
the archive.
+ `WSDLToAxisServiceBuilder` installs a second, separate resolver where a
+ caller supplied none, refusing *every* absolute `schemaLocation` -- `file:`
+ included -- so that only a relative location resolves, against the WSDL's own
+ base URI. Until 2.0.2 it classified with `java.net.URI` and swallowed
+ `URISyntaxException`. `URI` enforces RFC 2396 and rejects a location holding a
+ space, `|`, `{`, `}`, `^` or a backslash, while the `java.net.URL` the delegate
+ builds accepts them, so such a location was never classified and was resolved as
+ if relative -- the absolute remote fetch the guard exists to refuse, reached by
+ being malformed. Classification is now textual (a scheme prefix, or a `//`
+ network-path reference, which takes the base document's scheme) and cannot fail
+ open. A relative name that `URI` also rejects, such as one containing a space,
+ still resolves.
+
`file:` was added to the guard in 2.0.2. Only `AARBasedWSDLLocator` had
been letting one through: it counts `file:` as absolute, so an absolute
`file:` import passed the scheme check and fell through to the parent
@@ -404,8 +417,23 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
Separately, the `?xsd=` route reaches a service's packaged META-INF with
the request's value, and that directory holds `services.xml`, whose
parameters name keystores and password-callback classes. Only schema and
- WSDL documents are servable, enforced inside the shared stream helper so
- every caller inherits it rather than repeating it.
+ WSDL documents are servable, and only from the queried service's own
+ archive: the lookup uses `URLClassLoader.findResource`, which does not
+ delegate, so a name cannot reach a `META-INF` resource in an unrelated jar
+ further up the chain -- Axis2's own jars, `WEB-INF/lib`, or the container's
+ shared libraries. That is the AXIS2-5846 rule, and in 2.0.2 it holds on every
+ route: `AxisService.printXSD` and `printWSDL2` had kept using
+ `getResourceAsStream`, which delegates parent-first, so the `?xsd=` and
+ `?wsdl2=` queries searched the whole hierarchy while the file routes did not.
+ Both rules now live in one kernel helper, `MetaInfResources`, which the
+ transport delegates to, because two copies of this check are what let the two
+ routes drift apart in the first place.
+
+ A classloader that is not a `URLClassLoader` cannot be searched without
+ delegating, so this route serves nothing there rather than serving too much.
+ Deployed archives always get a `DeploymentClassLoader`, which is one; an
+ embedder building an `AxisService` against some other classloader loses this
+ route and keeps every other way of publishing a schema.
A hidden service is answered exactly as an undeployed one, on every route,
body included: the query routes no longer send 403 where an absent service
@@ -424,6 +452,24 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
URI, SOAPAction and WS-Addressing binds the service before the Security phase
and is unaffected.
+14. **Generated endpoint addresses depend on deployment configuration
+ (documentation, 2.0.2):** Set `httpFrontendHostUrl` in `axis2.xml` -- or an
+ explicit `port` parameter on the servlet transport -- for any deployment whose
+ published WSDL is consumed by others. Neither is set by default, and without
+ them `AxisServlet` autodetects its port from the first request after startup
+ and keeps it for the lifetime of the process. In a standard container
+ `getServerPort()` derives from the client's `Host` header, so the first caller
+ after a deployment or restart decides the port written into the `soap:address`
+ and EPRs served to every later client. The host part is taken per request and
+ reflects only to the requester; the port is shared state.
+
+ This is not fixed in code. Reading the actual listening port instead would
+ return the back-end port and publish a wrong address for every deployment
+ behind a TLS-terminating proxy, which is the common case, and deriving the
+ port per request means changing `TransportListener.getEPRsForService`, which
+ `ListenerManager` also calls at startup with no request in scope. Configure
+ the front-end URL; it is the only answer that is correct for both.
+
## Reporting Security Issues
Report vulnerabilities to: **security@apache.org**
diff --git a/modules/kernel/src/org/apache/axis2/description/AxisService.java b/modules/kernel/src/org/apache/axis2/description/AxisService.java
index 4b39449362..531b0372aa 100644
--- a/modules/kernel/src/org/apache/axis2/description/AxisService.java
+++ b/modules/kernel/src/org/apache/axis2/description/AxisService.java
@@ -57,6 +57,7 @@ import org.apache.axis2.i18n.Messages;
import org.apache.axis2.phaseresolver.PhaseResolver;
import org.apache.axis2.kernel.TransportListener;
import org.apache.axis2.util.IOUtils;
+import org.apache.axis2.util.MetaInfResources;
import org.apache.axis2.util.JavaUtils;
import org.apache.axis2.util.Loader;
import org.apache.axis2.util.LoggingControl;
@@ -1397,11 +1398,11 @@ public class AxisService extends AxisDescription {
schema.write(new OutputStreamWriter(out, "UTF8"));
out.flush();
} else {
- // make sure we are only serving .xsd files and ignore requests with
- // ".." in the name.
- if (xsd.endsWith(".xsd") && xsd.indexOf("..") == -1) {
- InputStream in = getClassLoader().getResourceAsStream(
- DeploymentConstants.META_INF + "/" + xsd);
+ // Only .xsd files, and only from this service's own archive:
+ // MetaInfResources does not delegate to ancestor classloaders, so
+ // the name cannot reach a schema packaged in an unrelated jar.
+ if (xsd.endsWith(".xsd")) {
+ InputStream in = MetaInfResources.getResourceAsStream(this, xsd);
if (in != null) {
IOUtils.copy(in, out, true);
} else {
@@ -1873,11 +1874,10 @@ public class AxisService extends AxisDescription {
// if the wsdl2 parameter is not empty or null in the requested URL, get the wsdl from the META-INF and serve.
//else construct the wsdl out of axis service and serve.
if ((wsdl != null ) && (!"".equals(wsdl))) {
- // make sure we are only serving .wsdl files and ignore requests with
- // ".." in the name.
- if (wsdl.endsWith(".wsdl") && wsdl.indexOf("..") == -1) {
- InputStream in = getClassLoader().getResourceAsStream(
- DeploymentConstants.META_INF + "/" + wsdl);
+ // Only .wsdl files, and only from this service's own archive; see the
+ // note on the ?xsd= route above.
+ if (wsdl.endsWith(".wsdl")) {
+ InputStream in = MetaInfResources.getResourceAsStream(this, wsdl);
if (in != null) {
IOUtils.copy(in, out, true);
} else {
diff --git a/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java b/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java
index 560f4ea544..805e28f006 100644
--- a/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java
+++ b/modules/kernel/src/org/apache/axis2/description/WSDLToAxisServiceBuilder.java
@@ -146,8 +146,10 @@ public abstract class WSDLToAxisServiceBuilder {
// resolution (SSRF via absolute schemaLocation URLs). The
// default URIResolver in xmlschema-core follows any absolute
// URI including http://, https://, ftp://, and jar://.
- // Local file:// and relative paths are allowed for co-packaged
- // schemas in .aar/.war deployments.
+ // Every absolute location is refused, file:// included; only a
+ // relative path resolves, against the base URI of the WSDL
+ // document, which is how co-packaged schemas in .aar/.war
+ // deployments refer to each other.
schemaCollection.setSchemaResolver(
new org.apache.ws.commons.schema.resolver.URIResolver() {
private final org.apache.ws.commons.schema.resolver.DefaultURIResolver
@@ -158,17 +160,11 @@ public abstract class WSDLToAxisServiceBuilder {
// SSRF and LFI. Relative paths (e.g., "wsat.xsd")
// are safe — they resolve against the local base
// URI of the WSDL document.
- if (loc != null) {
- try {
- java.net.URI locUri = new java.net.URI(loc);
- if (locUri.isAbsolute()) {
- throw new RuntimeException(
- "Absolute schemaLocation blocked: "
- + loc + " (use setCustomResolver"
- + " to opt in)");
- }
- } catch (java.net.URISyntaxException ignored) {
- }
+ if (isAbsoluteSchemaLocation(loc)) {
+ throw new RuntimeException(
+ "Absolute schemaLocation blocked: "
+ + loc + " (use setCustomResolver"
+ + " to opt in)");
}
return delegate.resolveEntity(ns, loc, base);
}
@@ -178,6 +174,48 @@ public abstract class WSDLToAxisServiceBuilder {
return schemaCollection.read(element);
}
+ /**
+ * Whether a schemaLocation names an absolute location, and so must not be
+ * resolved by the default resolver.
+ * <p>
+ * Classified textually rather than with {@link java.net.URI}, which is what this
+ * used to do. {@code URI} enforces RFC 2396 and throws on a location containing a
+ * space or another illegal character, while {@code java.net.URL} -- which the
+ * delegate builds -- accepts many of those. A {@code URISyntaxException} therefore
+ * meant "unclassified", and the guard caught it, treated the location as relative
+ * and resolved it: exactly the absolute remote locations it exists to refuse got
+ * through, by being malformed. A guard that cannot classify its input must not
+ * pass it.
+ *
+ * @param loc the schemaLocation as the document gave it
+ * @return true if it carries a scheme, or is a network-path reference
+ */
+ static boolean isAbsoluteSchemaLocation(String loc) {
+ if (loc == null) {
+ return false;
+ }
+ String trimmed = loc.trim();
+ // A network-path reference inherits the base document's scheme, so it is
+ // remote whenever the base is.
+ if (trimmed.startsWith("//")) {
+ return true;
+ }
+ // RFC 3986 scheme: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":"
+ for (int i = 0; i < trimmed.length(); i++) {
+ char c = trimmed.charAt(i);
+ if (c == ':') {
+ return i > 0;
+ }
+ boolean schemeChar = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
+ || (i > 0 && ((c >= '0' && c <= '9')
+ || c == '+' || c == '-' || c == '.'));
+ if (!schemeChar) {
+ return false;
+ }
+ }
+ return false;
+ }
+
/**
* Find the XML schema prefix
*
diff --git a/modules/kernel/src/org/apache/axis2/util/MetaInfResources.java b/modules/kernel/src/org/apache/axis2/util/MetaInfResources.java
new file mode 100644
index 0000000000..4fc221f6a4
--- /dev/null
+++ b/modules/kernel/src/org/apache/axis2/util/MetaInfResources.java
@@ -0,0 +1,106 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Locale;
+
+import org.apache.axis2.description.AxisService;
+
+/**
+ * Serves a schema or WSDL document that a service archive packages under
+ * {@code META-INF/}, by the name the request asked for.
+ * <p>
+ * Two properties have to hold on every route that does this, and they used to hold on
+ * some and not others, which is why the lookup lives in one place now:
+ * <ul>
+ * <li><b>Only schema and WSDL documents.</b> A service archive's {@code META-INF} holds
+ * more than those -- {@code services.xml}, whose parameters name keystores and
+ * password-callback classes, plus {@code MANIFEST.MF} and module policies.</li>
+ * <li><b>Only the queried service's own archive.</b> The lookup uses
+ * {@link URLClassLoader#findResource}, which does not delegate to the parent, so a
+ * request cannot reach a {@code META-INF} resource in an unrelated jar further up
+ * the chain -- Axis2's own jars, {@code WEB-INF/lib}, or the container's shared
+ * libraries. That is the AXIS2-5846 rule.</li>
+ * </ul>
+ * A classloader that is not a {@link URLClassLoader} cannot be searched without
+ * delegating, so nothing is served in that case rather than serving too much. Deployed
+ * archives always get a {@code DeploymentClassLoader}, which is one; an embedder that
+ * builds an {@code AxisService} against some other classloader loses this route and
+ * keeps every other way of publishing a schema.
+ */
+public class MetaInfResources {
+
+ private MetaInfResources() {
+ }
+
+ /**
+ * Whether a requested name may be served at all: a named {@code .xsd} or
+ * {@code .wsdl}, with no traversal, no scheme and no absolute path.
+ *
+ * @param name the name as the request gave it
+ * @return true if it is a document this route is allowed to serve
+ */
+ public static boolean isServable(String name) {
+ if (name == null || name.isEmpty()) {
+ return false;
+ }
+ String lower = name.toLowerCase(Locale.ENGLISH);
+ int extension = lower.endsWith(".xsd") ? 4 : (lower.endsWith(".wsdl") ? 5 : 0);
+ if (extension == 0) {
+ return false;
+ }
+ // Require something to be named, so that a bare ".xsd" is not a document.
+ String base = name.substring(0, name.length() - extension);
+ if (base.isEmpty() || base.endsWith("/")) {
+ return false;
+ }
+ return name.indexOf("..") < 0 && name.indexOf(':') < 0 && !name.startsWith("/");
+ }
+
+ /**
+ * Opens a packaged document from the service's own archive.
+ *
+ * @param service the service whose archive is being asked
+ * @param name the document name as the request gave it
+ * @return a stream over the document, or null if it is not servable, not present,
+ * or the service's classloader cannot be searched without delegating
+ */
+ public static InputStream getResourceAsStream(AxisService service, String name) {
+ if (service == null || !isServable(name)) {
+ return null;
+ }
+ ClassLoader classLoader = service.getClassLoader();
+ if (!(classLoader instanceof URLClassLoader)) {
+ return null;
+ }
+ URL url = ((URLClassLoader) classLoader).findResource("META-INF/" + name);
+ if (url == null) {
+ return null;
+ }
+ try {
+ return url.openStream();
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+}
diff --git a/modules/kernel/test/org/apache/axis2/description/AbsoluteSchemaLocationTest.java b/modules/kernel/test/org/apache/axis2/description/AbsoluteSchemaLocationTest.java
new file mode 100644
index 0000000000..298591ba92
--- /dev/null
+++ b/modules/kernel/test/org/apache/axis2/description/AbsoluteSchemaLocationTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.description;
+
+import junit.framework.TestCase;
+
+/**
+ * The default schema resolver refuses an absolute schemaLocation. It decided what was
+ * absolute with java.net.URI and swallowed URISyntaxException, so a location URI
+ * rejects but java.net.URL accepts -- one containing a space, say -- was classified as
+ * relative and fetched. These pin the classification, including the cases that used to
+ * be unclassified.
+ */
+public class AbsoluteSchemaLocationTest extends TestCase {
+
+ private void assertAbsolute(String loc) {
+ assertTrue("should be refused: " + loc,
+ WSDLToAxisServiceBuilder.isAbsoluteSchemaLocation(loc));
+ }
+
+ private void assertRelative(String loc) {
+ assertFalse("should still resolve: " + loc,
+ WSDLToAxisServiceBuilder.isAbsoluteSchemaLocation(loc));
+ }
+
+ public void testOrdinaryAbsoluteLocationsAreRefused() {
+ assertAbsolute("http://internal-host/schema.xsd");
+ assertAbsolute("https://internal-host/schema.xsd");
+ assertAbsolute("ftp://internal-host/schema.xsd");
+ assertAbsolute("jar:file:/tmp/a.jar!/schema.xsd");
+ assertAbsolute("file:///etc/passwd");
+ }
+
+ /**
+ * The bypass: java.net.URI throws on the space, so the old guard caught, gave up
+ * on classifying, and resolved it anyway.
+ */
+ public void testAnAbsoluteLocationWithAUriIllegalCharacterIsStillRefused() {
+ assertAbsolute("http://169.254.169.254/latest/meta-data/iam a.xsd");
+ assertAbsolute("http://internal-host/a|b.xsd");
+ assertAbsolute("http://internal-host/a{b}.xsd");
+ assertAbsolute("http://internal-host/a^b.xsd");
+ assertAbsolute("http://internal-host/a\\b.xsd");
+ }
+
+ /** Leading whitespace must not hide the scheme either. */
+ public void testWhitespaceDoesNotHideAScheme() {
+ assertAbsolute(" http://internal-host/schema.xsd");
+ assertAbsolute("\thttps://internal-host/schema.xsd");
+ }
+
+ /** A network-path reference takes the base document's scheme. */
+ public void testANetworkPathReferenceIsRefused() {
+ assertAbsolute("//internal-host/schema.xsd");
+ }
+
+ /** The ordinary co-packaged case must keep working; that is the whole point. */
+ public void testRelativeLocationsStillResolve() {
+ assertRelative("wsat.xsd");
+ assertRelative("./wsat.xsd");
+ assertRelative("../common/wsat.xsd");
+ assertRelative("schemas/wsat.xsd");
+ assertRelative("/absolute/path/wsat.xsd");
+ assertRelative(null);
+ assertRelative("");
+ }
+
+ /**
+ * A relative name that java.net.URI also rejects. It resolved before and must
+ * keep resolving: failing closed on everything URI dislikes would have broken
+ * ordinary deployments rather than the attack.
+ */
+ public void testARelativeNameWithASpaceStillResolves() {
+ assertRelative("my schema.xsd");
+ assertRelative("schemas/my schema.xsd");
+ }
+
+ /**
+ * A Windows absolute path is refused. It parses as a one-letter scheme, and the
+ * forward-slash form "C:/schemas/a.xsd" was already refused; only the backslash
+ * form slipped through, by being URI-invalid. An absolute local path is the LFI
+ * case this guard is for, so the two forms agreeing is the fix, not a casualty.
+ */
+ public void testAWindowsAbsolutePathIsRefusedInBothForms() {
+ assertAbsolute("C:/schemas/a.xsd");
+ assertAbsolute("C:\\schemas\\a.xsd");
+ }
+
+ /** A scheme needs a leading letter, so these are names, not locations. */
+ public void testThingsThatOnlyLookLikeSchemes() {
+ assertRelative("2foo:bar.xsd");
+ assertRelative(":leading-colon.xsd");
+ }
+}
diff --git a/modules/kernel/test/org/apache/axis2/util/MetaInfResourcesTest.java b/modules/kernel/test/org/apache/axis2/util/MetaInfResourcesTest.java
new file mode 100644
index 0000000000..3256134bbc
--- /dev/null
+++ b/modules/kernel/test/org/apache/axis2/util/MetaInfResourcesTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+
+import junit.framework.TestCase;
+
+import org.apache.axis2.description.AxisService;
+
+/**
+ * The ?xsd= and ?wsdl2= routes serve a packaged document by the name the request
+ * asked for. Two rules hold: only schema and WSDL documents, and only from the
+ * queried service's own archive -- the AXIS2-5846 rule, which the transport route
+ * followed and AxisService.printXSD/printWSDL2 did not, since they used
+ * getResourceAsStream and so searched the whole ancestor chain.
+ */
+public class MetaInfResourcesTest extends TestCase {
+
+ private File root;
+ private File parentRoot;
+ private URLClassLoader parent;
+ private URLClassLoader loader;
+
+ @Override
+ protected void setUp() throws Exception {
+ root = makeArchive("own", "mine.xsd", "<own/>");
+ parentRoot = makeArchive("parent", "theirs.xsd", "<theirs/>");
+ parent = new URLClassLoader(new URL[] { parentRoot.toURI().toURL() }, null);
+ loader = new URLClassLoader(new URL[] { root.toURI().toURL() }, parent);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (loader != null) {
+ loader.close();
+ }
+ if (parent != null) {
+ parent.close();
+ }
+ delete(root);
+ delete(parentRoot);
+ }
+
+ private File makeArchive(String prefix, String name, String content) throws Exception {
+ File dir = File.createTempFile("axis2-" + prefix, "");
+ dir.delete();
+ File metaInf = new File(dir, "META-INF");
+ assertTrue(metaInf.mkdirs());
+ FileOutputStream out = new FileOutputStream(new File(metaInf, name));
+ try {
+ out.write(content.getBytes(StandardCharsets.UTF_8));
+ } finally {
+ out.close();
+ }
+ return dir;
+ }
+
+ private void delete(File file) {
+ if (file == null) {
+ return;
+ }
+ File[] children = file.listFiles();
+ if (children != null) {
+ for (int i = 0; i < children.length; i++) {
+ delete(children[i]);
+ }
+ }
+ file.delete();
+ }
+
+ private AxisService serviceOn(ClassLoader classLoader) {
+ AxisService service = new AxisService("Test");
+ service.setClassLoader(classLoader);
+ return service;
+ }
+
+ private String read(InputStream in) throws Exception {
+ assertNotNull(in);
+ try {
+ byte[] buffer = new byte[256];
+ int n = in.read(buffer);
+ return new String(buffer, 0, n, StandardCharsets.UTF_8);
+ } finally {
+ in.close();
+ }
+ }
+
+ public void testTheServicesOwnDocumentIsServed() throws Exception {
+ InputStream in = MetaInfResources.getResourceAsStream(serviceOn(loader), "mine.xsd");
+ assertEquals("<own/>", read(in));
+ }
+
+ /** The finding: getResourceAsStream delegates parent-first and would find this. */
+ public void testADocumentInAnAncestorClassloaderIsNotServed() throws Exception {
+ assertNotNull("the ancestor really does carry it, so the test means something",
+ loader.getResourceAsStream("META-INF/theirs.xsd"));
+ assertNull("a resource from an unrelated jar must not be reachable by name",
+ MetaInfResources.getResourceAsStream(serviceOn(loader), "theirs.xsd"));
+ }
+
+ public void testAMissingDocumentIsNotFound() {
+ assertNull(MetaInfResources.getResourceAsStream(serviceOn(loader), "absent.xsd"));
+ }
+
+ /**
+ * A classloader that is not a URLClassLoader cannot be searched without
+ * delegating, so this route serves nothing rather than serving the whole chain.
+ */
+ public void testANonUrlClassloaderServesNothing() {
+ ClassLoader plain = new ClassLoader(loader) {
+ };
+ assertNull(MetaInfResources.getResourceAsStream(serviceOn(plain), "mine.xsd"));
+ }
+
+ public void testANullServiceIsHandled() {
+ assertNull(MetaInfResources.getResourceAsStream(null, "mine.xsd"));
+ }
+
+ public void testOnlySchemaAndWsdlDocumentsAreServable() {
+ assertTrue(MetaInfResources.isServable("a.xsd"));
+ assertTrue(MetaInfResources.isServable("a.wsdl"));
+ assertTrue(MetaInfResources.isServable("sub/a.xsd"));
+ assertFalse("services.xml names keystores and callback classes",
+ MetaInfResources.isServable("services.xml"));
+ assertFalse(MetaInfResources.isServable("MANIFEST.MF"));
+ assertFalse(MetaInfResources.isServable(null));
+ assertFalse(MetaInfResources.isServable(""));
+ }
+
+ public void testTraversalAbsoluteAndSchemeNamesAreRefused() {
+ assertFalse(MetaInfResources.isServable("../../services.xml"));
+ assertFalse(MetaInfResources.isServable("../a.xsd"));
+ assertFalse(MetaInfResources.isServable("/etc/a.xsd"));
+ assertFalse(MetaInfResources.isServable("http://elsewhere/a.xsd"));
+ assertFalse("a bare extension is not a document", MetaInfResources.isServable(".xsd"));
+ assertFalse(MetaInfResources.isServable("sub/.wsdl"));
+ }
+
+ /** Traversal is refused whatever the archive holds, not merely not found. */
+ public void testTraversalIsRefusedBeforeAnyLookup() throws Exception {
+ assertNull(MetaInfResources.getResourceAsStream(
+ serviceOn(loader), "../META-INF/mine.xsd"));
+ }
+}
diff --git a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java
index f33d36f479..86abb8c73f 100644
--- a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java
+++ b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPTransportUtils.java
@@ -37,6 +37,7 @@ import org.apache.axis2.builder.BuilderUtil;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.AxisService;
+import org.apache.axis2.util.MetaInfResources;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.engine.AxisConfiguration;
@@ -53,11 +54,8 @@ import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Locale;
import java.io.OutputStream;
import java.net.SocketException;
-import java.net.URL;
-import java.net.URLClassLoader;
import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Map;
@@ -455,52 +453,16 @@ public class HTTPTransportUtils {
}
static InputStream getMetaInfResourceAsStream(AxisService service, String name) {
- // Only the packaged schema and WSDL documents are servable here. One caller,
- // the ?xsd= route, reaches this with the request's value verbatim, and a
- // service archive's META-INF holds more than schemas: services.xml, whose
- // parameters name keystores and password-callback classes, plus MANIFEST.MF
- // and module policies. Guarding inside this method rather than at each call
- // site means a future caller inherits the restriction instead of having to
- // remember it.
- if (!isServableMetadataResource(name)) {
- return null;
- }
- ClassLoader classLoader = service.getClassLoader();
- if (classLoader instanceof URLClassLoader) {
- // Only search the service class loader and skip searching the ancestors to
- // avoid local file inclusion vulnerabilities such as AXIS2-5846.
- URL url = ((URLClassLoader)classLoader).findResource("META-INF/" + name);
- try {
- return url == null ? null : url.openStream();
- } catch (IOException ex) {
- return null;
- }
- } else {
- return null;
- }
+ // The lookup itself lives in the kernel, because AxisService.printXSD and
+ // printWSDL2 serve the same documents by name and have to obey the same two
+ // rules -- schemas and WSDLs only, and only from the queried service's own
+ // archive (AXIS2-5846). Keeping one implementation is what stops those two
+ // routes from drifting apart again.
+ return MetaInfResources.getResourceAsStream(service, name);
}
- /**
- * Whether a request-supplied META-INF resource name may be served.
- *
- * @param name the resource name, relative to META-INF
- * @return true only for a schema or WSDL document that stays inside META-INF
- */
static boolean isServableMetadataResource(String name) {
- if (name == null || name.isEmpty()) {
- return false;
- }
- String lower = name.toLowerCase(Locale.ENGLISH);
- int extension = lower.endsWith(".xsd") ? 4 : (lower.endsWith(".wsdl") ? 5 : 0);
- if (extension == 0) {
- return false;
- }
- // Require something to be named, so that a bare ".xsd" is not a document.
- String base = name.substring(0, name.length() - extension);
- if (base.isEmpty() || base.endsWith("/")) {
- return false;
- }
- return name.indexOf("..") < 0 && name.indexOf(':') < 0 && !name.startsWith("/");
+ return MetaInfResources.isServable(name);
}
/**
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index 905e9210c1..be07704afb 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -164,6 +164,30 @@ 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.
+- **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
+ parent-first, so an anonymous caller could read any `META-INF/*.xsd` or `*.wsdl` from
+ jars anywhere on the webapp or container classpath and fingerprint what was deployed.
+ The file routes had been fixed for this once (AXIS2-5846); the query routes had not.
+ Both rules -- schemas and WSDLs only, own archive only -- now live in one kernel
+ helper, `MetaInfResources`. A service whose classloader is not a `URLClassLoader`
+ cannot serve this route at all; deployed archives are unaffected.
+
+- **The absolute-`schemaLocation` guard no longer fails open on malformed input.**
+ `WSDLToAxisServiceBuilder`'s default schema resolver classified locations with
+ `java.net.URI` and ignored `URISyntaxException`. A location `URI` rejects but
+ `java.net.URL` accepts -- one containing a space, `|`, `{`, `}`, `^` or a backslash --
+ was therefore never classified and was resolved as though relative, fetching the
+ remote schema the guard exists to refuse. Classification is textual now. Relative
+ names that `URI` also rejects still resolve.
+
+- **`httpFrontendHostUrl` is documented as a requirement, not a convenience.** Without
+ it (or an explicit servlet-transport `port`), the port advertised in generated WSDL
+ comes from the first request's `Host` header after startup and is kept for the life of
+ the process. See item 14 of `SECURITY.md`; this is a configuration note, not a code
+ change.
+
- **Two threat-model claims made true, and their limits stated.** The document said
every parser factory the framework creates disables DTDs and external entities, and
that the AAR/WAR resolvers block `file:` resolution. Neither held. The SAAJ and
(axis-axis2-java-core) 01/13: Filter the service listing in the model, not only in the view
Posted by ro...@apache.org.
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 8be5bb57231e7c36745d3ae897a7376b22949af1
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 08:59:11 2026 -1000
Filter the service listing in the model, not only in the view
The servlet listing did exclude hiddenService services, but in listServices.jsp
rather than before the model reached the request, so anything else rendering
that model would name them. Apply both controls where the model is built, as
HTTPTransportReceiver does for the standalone transport, and drop a null
service instead of carrying one: isHiddenService would throw on it, and the
previous putAll had the same hazard.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../apache/axis2/transport/http/ListingAgent.java | 13 ++-
.../transport/http/ServiceListingFilterTest.java | 111 +++++++++++++++++++++
2 files changed, 122 insertions(+), 2 deletions(-)
diff --git a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
index cece74534c..a81408c414 100644
--- a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
+++ b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
@@ -358,8 +358,17 @@ public class ListingAgent extends AbstractAgent {
// either: the listing gives its name, EPR and every operation, which
// is what those routes were hidden to withhold.
for (java.util.Map.Entry<String, AxisService> entry : services.entrySet()) {
- if (entry.getValue() == null || entry.getValue().isMetadataExposed()) {
- sortedServices.put(entry.getKey(), entry.getValue());
+ AxisService service = entry.getValue();
+ // Filter here rather than relying on the view: a hidden service
+ // that reaches the request attributes can still be rendered by
+ // any other consumer of it. hiddenService and metadata exposure
+ // are separate controls and both withhold the listing, which is
+ // the same pair HTTPTransportReceiver applies for the standalone
+ // transport. A null cannot be rendered at all -- isHiddenService
+ // would throw on it -- so it is dropped rather than carried.
+ if (service != null && !Utils.isHiddenService(service)
+ && service.isMetadataExposed()) {
+ sortedServices.put(entry.getKey(), service);
}
}
}
diff --git a/modules/transport/http/src/test/java/org/apache/axis2/transport/http/ServiceListingFilterTest.java b/modules/transport/http/src/test/java/org/apache/axis2/transport/http/ServiceListingFilterTest.java
new file mode 100644
index 0000000000..a302276f59
--- /dev/null
+++ b/modules/transport/http/src/test/java/org/apache/axis2/transport/http/ServiceListingFilterTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.transport.http;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.apache.axis2.Constants;
+import org.apache.axis2.context.ConfigurationContext;
+import org.apache.axis2.context.ConfigurationContextFactory;
+import org.apache.axis2.description.AxisService;
+
+/**
+ * The service listing is filtered before it reaches the request attributes rather
+ * than only in the view, so that a service the operator withheld cannot be rendered
+ * by any other consumer of the model.
+ *
+ * <p>Two separate controls withhold a service: the {@code hiddenService} parameter,
+ * and {@code exposeServiceMetadata}. The standalone transport's lister applies both,
+ * and so does this one.
+ */
+public class ServiceListingFilterTest extends TestCase {
+
+ private ConfigurationContext configContext;
+
+ @Override
+ protected void setUp() throws Exception {
+ configContext = ConfigurationContextFactory.createEmptyConfigurationContext();
+
+ configContext.getAxisConfiguration().addService(new AxisService("Visible"));
+
+ AxisService notExposed = new AxisService("NotExposed");
+ notExposed.addParameter(AxisService.EXPOSE_SERVICE_METADATA, "false");
+ configContext.getAxisConfiguration().addService(notExposed);
+
+ AxisService hidden = new AxisService("HiddenParam");
+ hidden.addParameter(Constants.HIDDEN_SERVICE_PARAM_NAME, "true");
+ configContext.getAxisConfiguration().addService(hidden);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, AxisService> listedServices() throws Exception {
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ HttpServletResponse res = mock(HttpServletResponse.class);
+ when(req.getRequestURL())
+ .thenReturn(new StringBuffer("http://localhost:8080/axis2/services/"));
+ final Map<String, AxisService>[] captured = new Map[1];
+ // renderView needs a container, so capture the model the JSP would receive.
+ org.mockito.Mockito.doAnswer(invocation -> {
+ if ("sortedServices".equals(invocation.getArgument(0))) {
+ captured[0] = (Map<String, AxisService>) invocation.getArgument(1);
+ }
+ return null;
+ }).when(req).setAttribute(org.mockito.ArgumentMatchers.anyString(),
+ org.mockito.ArgumentMatchers.any());
+ try {
+ new ListingAgent(configContext).processListServices(req, res);
+ } catch (Exception rendering) {
+ // The view cannot render without a container; the model is what matters.
+ }
+ assertNotNull("the listing model should have been populated", captured[0]);
+ return captured[0];
+ }
+
+ public void testAnExposedServiceIsListed() throws Exception {
+ assertTrue(listedServices().containsKey("Visible"));
+ }
+
+ public void testAServiceWithMetadataExposureDisabledIsNotListed() throws Exception {
+ assertFalse("the listing names the service, its EPR and its operations",
+ listedServices().containsKey("NotExposed"));
+ }
+
+ /**
+ * The hiddenService parameter is a separate control, and the servlet listing has
+ * to apply it in the model too -- not only in listServices.jsp.
+ */
+ public void testAServiceHiddenByParameterIsNotListed() throws Exception {
+ assertFalse(listedServices().containsKey("HiddenParam"));
+ }
+
+ /** Nothing null reaches the model, since the view would throw on it. */
+ public void testNoNullServiceReachesTheModel() throws Exception {
+ for (AxisService service : listedServices().values()) {
+ assertNotNull("a null service would break rendering the page", service);
+ }
+ }
+}
(axis-axis2-java-core) 07/13: Pin the Swagger UI scripts, and stop naming exception classes in faults
Posted by ro...@apache.org.
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 00274eedfaf5e20b0160f4267271182646884170
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 15:39:41 2026 -1000
Pin the Swagger UI scripts, and stop naming exception classes in faults
Two low-severity leaks on the JSON surface. The Swagger UI page loads its
scripts from a CDN at a pinned version, which says which release to request but
not that what arrived is that release, on a page built to drive authenticated
API calls; the tags now carry SHA-384 integrity hashes, taken from two
independent CDNs and confirmed identical. They are version-specific, so a
deployment pinning another version supplies its own and is warned when it has
not. Separately, an unhandled Throwable produced a fault reason of
fully.qualified.Class: message regardless of sendStacktraceDetailsWithFaults,
on JSON and REST responses as much as SOAP faults, since that catch-all serves
every request the servlet handles; it is generic now, and still logged in full.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../apache/axis2/openapi/OpenApiConfiguration.java | 24 +++++
.../org/apache/axis2/openapi/SwaggerUIHandler.java | 70 +++++++++++++-
.../axis2/openapi/SwaggerUIIntegrityTest.java | 106 +++++++++++++++++++++
.../apache/axis2/transport/http/AxisServlet.java | 28 +++++-
src/site/markdown/release-notes/2.0.2.md | 16 ++++
5 files changed, 238 insertions(+), 6 deletions(-)
diff --git a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java
index a9e4ad1cea..8f0787776c 100644
--- a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java
+++ b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java
@@ -144,6 +144,14 @@ public class OpenApiConfiguration {
/** Swagger UI version to use */
private String swaggerUiVersion = "5.32.13";
+ /**
+ * Subresource Integrity hashes for the Swagger UI scripts, when the deployment
+ * pins a version other than the one this release ships hashes for. A version
+ * pin alone says which release to fetch, not that what arrived is that release.
+ */
+ private String swaggerUiBundleIntegrity;
+ private String swaggerUiStandalonePresetIntegrity;
+
/** Maven group and artifact for Swagger UI */
private String swaggerUiMavenGroupAndArtifact = "org.webjars:swagger-ui";
@@ -267,6 +275,10 @@ public class OpenApiConfiguration {
// Swagger UI
swaggerUiVersion = getProperty(props, "openapi.swaggerUi.version", swaggerUiVersion);
+ swaggerUiBundleIntegrity = getProperty(props,
+ "openapi.swaggerUi.integrity.bundle", swaggerUiBundleIntegrity);
+ swaggerUiStandalonePresetIntegrity = getProperty(props,
+ "openapi.swaggerUi.integrity.standalonePreset", swaggerUiStandalonePresetIntegrity);
// Resource packages (comma-separated)
String packages = getProperty(props, "openapi.resourcePackages", null);
@@ -428,6 +440,16 @@ public class OpenApiConfiguration {
public String getSwaggerUiVersion() { return swaggerUiVersion; }
public void setSwaggerUiVersion(String swaggerUiVersion) { this.swaggerUiVersion = swaggerUiVersion; }
+ public String getSwaggerUiBundleIntegrity() { return swaggerUiBundleIntegrity; }
+ public void setSwaggerUiBundleIntegrity(String swaggerUiBundleIntegrity) {
+ this.swaggerUiBundleIntegrity = swaggerUiBundleIntegrity;
+ }
+
+ public String getSwaggerUiStandalonePresetIntegrity() { return swaggerUiStandalonePresetIntegrity; }
+ public void setSwaggerUiStandalonePresetIntegrity(String swaggerUiStandalonePresetIntegrity) {
+ this.swaggerUiStandalonePresetIntegrity = swaggerUiStandalonePresetIntegrity;
+ }
+
public String getSwaggerUiMavenGroupAndArtifact() { return swaggerUiMavenGroupAndArtifact; }
public void setSwaggerUiMavenGroupAndArtifact(String swaggerUiMavenGroupAndArtifact) {
this.swaggerUiMavenGroupAndArtifact = swaggerUiMavenGroupAndArtifact;
@@ -523,6 +545,8 @@ public class OpenApiConfiguration {
copy.scannerClass = this.scannerClass;
copy.supportSwaggerUi = this.supportSwaggerUi;
copy.swaggerUiVersion = this.swaggerUiVersion;
+ copy.swaggerUiBundleIntegrity = this.swaggerUiBundleIntegrity;
+ copy.swaggerUiStandalonePresetIntegrity = this.swaggerUiStandalonePresetIntegrity;
copy.swaggerUiMavenGroupAndArtifact = this.swaggerUiMavenGroupAndArtifact;
copy.configLocation = this.configLocation;
copy.propertiesLocation = this.propertiesLocation;
diff --git a/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java b/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java
index 428c8cd1bd..80b853e538 100644
--- a/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java
+++ b/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java
@@ -63,6 +63,25 @@ public class SwaggerUIHandler {
// Default Swagger UI version (can be overridden by configuration)
private static final String DEFAULT_SWAGGER_UI_VERSION = "5.32.13";
+ /**
+ * Subresource Integrity hashes for {@link #DEFAULT_SWAGGER_UI_VERSION}.
+ * <p>
+ * Pinning the version in the URL says which release to ask the CDN for. It does
+ * not say that what came back is that release: the page is built to drive
+ * authenticated API calls, so a CDN serving modified script runs it with the
+ * reader's session. These are SHA-384 digests of the published 5.32.13 files,
+ * taken from two independent CDNs and confirmed byte-identical.
+ * <p>
+ * They are version-specific. A deployment pinning a different version supplies
+ * its own with {@code openapi.swaggerUi.integrity.bundle} and
+ * {@code openapi.swaggerUi.integrity.standalonePreset}; without them the page
+ * still renders, and a warning says integrity is not being enforced.
+ */
+ private static final String DEFAULT_BUNDLE_INTEGRITY =
+ "sha384-PsJla434CobCNv3y1K4wRavOqkUAvwGEQEfbUmI98CCqqGCJsmuDsgIjM6ZQQODP";
+ private static final String DEFAULT_STANDALONE_PRESET_INTEGRITY =
+ "sha384-IxiIENmaFuZnXUo3BucU89aVUoQ3Z6o2mk+SzTkxlsiYX23mun5v8eb2wbhsfEr8";
+
// Default resource paths
private static final String SWAGGER_UI_ROOT = "/swagger-ui/";
private static final String API_DOCS_PATH = "/api-docs/";
@@ -330,9 +349,17 @@ public class SwaggerUIHandler {
// Add Swagger UI scripts
html.append(" <script src=\"https://unpkg.com/swagger-ui-dist@")
- .append(swaggerUiVersion).append("/swagger-ui-bundle.js\"></script>\n")
+ .append(swaggerUiVersion).append("/swagger-ui-bundle.js\"")
+ .append(integrityAttributes(swaggerUiVersion,
+ configuration.getSwaggerUiBundleIntegrity(),
+ DEFAULT_BUNDLE_INTEGRITY, "swagger-ui-bundle.js"))
+ .append("></script>\n")
.append(" <script src=\"https://unpkg.com/swagger-ui-dist@")
- .append(swaggerUiVersion).append("/swagger-ui-standalone-preset.js\"></script>\n");
+ .append(swaggerUiVersion).append("/swagger-ui-standalone-preset.js\"")
+ .append(integrityAttributes(swaggerUiVersion,
+ configuration.getSwaggerUiStandalonePresetIntegrity(),
+ DEFAULT_STANDALONE_PRESET_INTEGRITY, "swagger-ui-standalone-preset.js"))
+ .append("></script>\n");
// Add Swagger UI initialization script
html.append(generateSwaggerUIScript(openApiUrl, scriptNonce));
@@ -489,12 +516,45 @@ public class SwaggerUIHandler {
return Base64.getEncoder().encodeToString(nonce);
}
+ /**
+ * Builds the integrity and crossorigin attributes for a CDN script tag.
+ *
+ * @param version the Swagger UI version being requested
+ * @param configured an operator-supplied hash, used whatever the version
+ * @param shipped the hash this release knows, valid only for the default version
+ * @param fileName named in the warning when no hash applies
+ * @return the attributes to add, or an empty string when no hash is known
+ */
+ private String integrityAttributes(String version, String configured,
+ String shipped, String fileName) {
+ String integrity = null;
+ if (configured != null && !configured.trim().isEmpty()) {
+ integrity = configured.trim();
+ } else if (DEFAULT_SWAGGER_UI_VERSION.equals(version)) {
+ integrity = shipped;
+ }
+ if (integrity == null) {
+ // Not fatal: refusing to render would turn a supply-chain hardening
+ // into an outage for anyone who pinned a different version.
+ log.warn("Serving " + fileName + " for Swagger UI version " + version
+ + " without Subresource Integrity: this release ships a hash for "
+ + DEFAULT_SWAGGER_UI_VERSION + " only. Set"
+ + " openapi.swaggerUi.integrity.bundle and"
+ + " openapi.swaggerUi.integrity.standalonePreset to pin this version.");
+ return "";
+ }
+ // crossorigin is required for the browser to check integrity cross-origin.
+ return " integrity=\"" + integrity + "\" crossorigin=\"anonymous\"";
+ }
+
/**
* Apply a Content-Security-Policy to the Swagger UI page.
*
- * <p>Only the nonced initialisation script and the pinned Swagger UI
- * distribution may execute, so script injected anywhere into this page does
- * not run even if an encoding defect is reintroduced. Operator-configured
+ * <p>Only the nonced initialisation script and the Swagger UI distribution may
+ * execute, so script injected anywhere into this page does not run even if an
+ * encoding defect is reintroduced. The policy names the CDN origin, which says
+ * where script may come from but not what it is; that is what the Subresource
+ * Integrity hashes on the script tags are for. Operator-configured
* custom CSS and JavaScript origins are added to the policy so that
* customised deployments keep working.
*/
diff --git a/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIIntegrityTest.java b/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIIntegrityTest.java
new file mode 100644
index 0000000000..6f54fdb27e
--- /dev/null
+++ b/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIIntegrityTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.openapi;
+
+import java.lang.reflect.Method;
+
+import org.apache.axis2.context.ConfigurationContext;
+import org.apache.axis2.context.ConfigurationContextFactory;
+
+import junit.framework.TestCase;
+
+/**
+ * The Swagger UI page is built to drive authenticated API calls, and its scripts come
+ * from a CDN. Pinning the version in the URL says which release to ask for, not that
+ * what came back is that release, so the script tags carry Subresource Integrity
+ * hashes. They are version-specific: a deployment pinning a different version has to
+ * supply its own, and the page still renders without them rather than going down.
+ */
+public class SwaggerUIIntegrityTest extends TestCase {
+
+ private String attributesFor(OpenApiConfiguration configuration, String version,
+ String configured, String shipped) throws Exception {
+ ConfigurationContext configContext =
+ ConfigurationContextFactory.createEmptyConfigurationContext();
+ SwaggerUIHandler handler = new SwaggerUIHandler(configContext, configuration);
+ Method method = SwaggerUIHandler.class.getDeclaredMethod("integrityAttributes",
+ String.class, String.class, String.class, String.class);
+ method.setAccessible(true);
+ return (String) method.invoke(handler, version, configured, shipped,
+ "swagger-ui-bundle.js");
+ }
+
+ public void testTheShippedHashIsUsedForTheDefaultVersion() throws Exception {
+ OpenApiConfiguration configuration = new OpenApiConfiguration();
+ String attributes = attributesFor(configuration,
+ configuration.getSwaggerUiVersion(), null, "sha384-SHIPPED");
+ assertTrue("the shipped hash should be applied, was: " + attributes,
+ attributes.contains("integrity=\"sha384-SHIPPED\""));
+ assertTrue("the browser only checks integrity cross-origin with this",
+ attributes.contains("crossorigin=\"anonymous\""));
+ }
+
+ /** A hash for one version says nothing about another, so it must not be reused. */
+ public void testTheShippedHashIsNotAppliedToAnotherVersion() throws Exception {
+ OpenApiConfiguration configuration = new OpenApiConfiguration();
+ String attributes = attributesFor(configuration, "4.15.5", null, "sha384-SHIPPED");
+ assertEquals("a hash from a different version would just break the page",
+ "", attributes);
+ }
+
+ /** Which is why an operator pinning a version can supply their own. */
+ public void testAnOperatorSuppliedHashIsUsedForAnyVersion() throws Exception {
+ OpenApiConfiguration configuration = new OpenApiConfiguration();
+ String attributes =
+ attributesFor(configuration, "4.15.5", "sha384-OPERATOR", "sha384-SHIPPED");
+ assertTrue("was: " + attributes, attributes.contains("integrity=\"sha384-OPERATOR\""));
+ assertTrue(attributes.contains("crossorigin=\"anonymous\""));
+ }
+
+ public void testAnOperatorHashWinsOverTheShippedOne() throws Exception {
+ OpenApiConfiguration configuration = new OpenApiConfiguration();
+ String attributes = attributesFor(configuration,
+ configuration.getSwaggerUiVersion(), "sha384-OPERATOR", "sha384-SHIPPED");
+ assertTrue(attributes.contains("sha384-OPERATOR"));
+ assertFalse(attributes.contains("sha384-SHIPPED"));
+ }
+
+ public void testABlankOperatorHashFallsBackRatherThanEmittingNothing() throws Exception {
+ OpenApiConfiguration configuration = new OpenApiConfiguration();
+ String attributes = attributesFor(configuration,
+ configuration.getSwaggerUiVersion(), " ", "sha384-SHIPPED");
+ assertTrue(attributes.contains("sha384-SHIPPED"));
+ }
+
+ /** The hashes shipped for the default version must actually be present. */
+ public void testTheDefaultVersionShipsRealHashes() throws Exception {
+ OpenApiConfiguration configuration = new OpenApiConfiguration();
+ String attributes = attributesFor(configuration,
+ configuration.getSwaggerUiVersion(), null,
+ readShippedHash("DEFAULT_BUNDLE_INTEGRITY"));
+ assertTrue("a sha384 SRI hash is expected, was: " + attributes,
+ attributes.contains("integrity=\"sha384-"));
+ }
+
+ private String readShippedHash(String fieldName) throws Exception {
+ java.lang.reflect.Field field = SwaggerUIHandler.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return (String) field.get(null);
+ }
+}
diff --git a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java
index 7faf7b5ebf..c34bc13f2c 100644
--- a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java
+++ b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java
@@ -241,7 +241,8 @@ public class AxisServlet extends HttpServlet {
}
}
}
- handleFault(msgContext, out, new AxisFault(t.toString(), t));
+ handleFault(msgContext, out,
+ new AxisFault(unexpectedErrorReason(msgContext, t), t));
} catch (AxisFault e2) {
log.info(e2);
throw new ServletException(e2);
@@ -483,6 +484,31 @@ public class AxisServlet extends HttpServlet {
}
}
+ /**
+ * The fault reason sent to the caller for an otherwise unhandled Throwable.
+ * <p>
+ * {@code Throwable.toString()} is {@code fully.qualified.Class: message}, and the
+ * reason goes to the client, so the framework's internals were disclosed whatever
+ * {@code sendStacktraceDetailsWithFaults} said. This catch-all serves every
+ * request the servlet handles, so that applied to JSON and REST responses as much
+ * as to SOAP faults. The Throwable is logged with its stack above either way, so
+ * a generic reason costs the operator nothing.
+ *
+ * @param msgContext the message being answered, may be null
+ * @param t the unhandled error
+ * @return the class name and message when details are enabled, otherwise generic
+ */
+ private String unexpectedErrorReason(MessageContext msgContext, Throwable t) {
+ if (msgContext != null) {
+ Parameter param = msgContext.getParameter(
+ Constants.Configuration.SEND_STACKTRACE_DETAILS_WITH_FAULTS);
+ if (param != null && JavaUtils.isTrue(param.getValue())) {
+ return t.toString();
+ }
+ }
+ return "Internal server error";
+ }
+
protected void handleFault(MessageContext msgContext, OutputStream out, AxisFault e)
throws AxisFault {
msgContext.setProperty(MessageContext.TRANSPORT_OUT, out);
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index 0dab023f84..2a2e0f835e 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -148,6 +148,22 @@ in `SECURITY.md`.
from the network; if you persist or replicate contexts, restrict the stream you
own.
+- **Swagger UI scripts are integrity-pinned.** The page loads swagger-ui from a CDN
+ at a pinned version, which says which release to ask for but not that what came
+ back is that release -- and the page is built to drive authenticated API calls. The
+ script tags now carry Subresource Integrity hashes and `crossorigin="anonymous"`.
+ The hashes are version-specific, so a deployment pinning a different Swagger UI
+ version supplies its own with `openapi.swaggerUi.integrity.bundle` and
+ `openapi.swaggerUi.integrity.standalonePreset`; without them the page still renders
+ and a warning says integrity is not being enforced.
+
+- **Unexpected-error faults no longer name the exception class.** An otherwise
+ unhandled error produced a fault reason of `fully.qualified.Class: message`
+ whatever `sendStacktraceDetailsWithFaults` was set to, and since that catch-all
+ serves every request the servlet handles, it applied to JSON and REST responses as
+ much as to SOAP faults. The reason is now generic unless details are enabled. The
+ error is logged with its stack as before.
+
- **OpenAPI and Swagger UI output.** Request-controlled values are validated and
encoded for the context they are written into, the served page carries a
Content-Security-Policy with a per-response script nonce, and the published
(axis-axis2-java-core) 11/13: Deprecate the mail transport for removal in 2.1.0
Posted by ro...@apache.org.
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 b5a2474b8c97ff4b50f0062693f6eb6597728a7c
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sat Sep 5 03:51:39 2026 -1000
Deprecate the mail transport for removal in 2.1.0
SOAP over SMTP was specified and profiled, but a request/response that blocks a
caller thread on a mailbox poll saw very little use even at SOAP's peak. No
functional change has landed in the module in over a decade; every commit
touching it since 2015 is a project-wide sweep or a test-dependency bump. It is
enabled by no default axis2.xml, yet axis2-webapp depends on it, so it ships to
everyone regardless. Release note only -- nothing is removed in 2.0.2.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
src/site/markdown/release-notes/2.0.2.md | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index 7bd095e8bd..f9b540e895 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -3,6 +3,28 @@ Apache Axis2 2.0.2 Release Notes
(Unreleased)
+## Deprecated for Removal
+
+- **The mail transport (`axis2-transport-mail`) is deprecated and is planned for
+ removal in 2.1.0.** SOAP over SMTP was specified and profiled, but a request/response
+ exchange that blocks a caller thread on a mailbox poll saw very little use even when
+ SOAP was at its most popular, and it shows: no functional change has been made to
+ this module in over a decade, and every commit touching it since 2015 is either a
+ project-wide sweep (the jakarta rename, the SecurityManager removal, a package move)
+ or a test-dependency bump.
+
+ It is not enabled by any default `axis2.xml`, but `axis2-webapp` depends on it, so
+ the module and its mail provider ship in the WAR and the binary distribution to every
+ deployment regardless. Its correlation of responses to requests by `In-Reply-To`
+ (`SynchronousCallback` and the callback table in `AbstractTransportListener`) is the
+ only such mechanism in the tree and is used by nothing else; the module's own source
+ carries long-standing `FIXME` comments saying that machinery does not belong where it
+ sits.
+
+ If you use the mail transport, please say so on the dev list -- concrete usage is
+ what would keep it. Otherwise plan to move to an HTTP transport before 2.1.0. Nothing
+ is removed in 2.0.2 and the module continues to work in this release.
+
## Security Hardening
Several defaults changed in this release. Each one closes a surface an
(axis-axis2-java-core) 02/13: Say what context deserialization actually enforces, and enforce that
Posted by ro...@apache.org.
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 84811c7c87192cd4aa96b3443f195cba2384617f
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 10:26:57 2026 -1000
Say what context deserialization actually enforces, and enforce that
SECURITY.md promised three times that SafeObjectInputStream restricts
deserialization to known Axis2 context classes. It never did, and it cannot:
the feature carries application objects on purpose -- self-managed data, and
Parameter values of any serializable type -- so a list of Axis2's own classes
would refuse what it exists to carry. The streams Axis2 creates now refuse
dynamic proxies, which it never writes and which front the classic gadget
chains, and accept a JEP 290 pattern for an integrator who does know their
types. The object-form path reads from the caller's own stream, which cannot
take a filter by then; the docs now say so rather than implying otherwise.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
SECURITY.md | 37 ++++-
.../externalize/ContextDeserializationFilter.java | 153 ++++++++++++++++++
.../context/externalize/SafeObjectInputStream.java | 8 +-
.../ContextDeserializationFilterTest.java | 178 +++++++++++++++++++++
src/site/markdown/release-notes/2.0.2.md | 13 ++
5 files changed, 383 insertions(+), 6 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 5a2825b04c..76475cfb4f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -138,7 +138,7 @@ Axis2 exposes the following URL patterns from the servlet mapping:
| **Form-urlencoded builder** | Unbounded body read into an in-memory map | `formUrlEncodedMaxRequestSize` bounds the read; the stream fails rather than truncating |
| **Service dispatchers** | Routing to unintended service; header spoofing; a service chosen from message content after the Security phase has already run | Dispatchers validate service existence and unknown services return fault; selecting the service from the SOAP body namespace is off unless `allowContentBasedServiceDispatch` is set, so binding happens before the Security phase runs |
| **Hot-deployment** (DeploymentEngine) | Malicious AAR/MAR deploys arbitrary code | Trust boundary is filesystem access; no signature verification (admin operation) |
-| **Context externalization** (SafeObjectInputStream) | Java deserialization gadget chains | Whitelist-based `SafeObjectInputStream`; restricted to known Axis2 context classes |
+| **Context externalization** (SafeObjectInputStream) | Java deserialization gadget chains | No class allowlist is possible: the feature exists to carry application objects (self-managed data, `Parameter` values). Streams Axis2 creates refuse dynamic proxies and honour `org.apache.axis2.context.externalize.serialFilter`; no Axis2 path feeds them from the network, and an integrator who persists or replicates contexts must restrict the stream they own |
| **Metadata endpoints** (`?wsdl`, `?xsd`, `/services/`, `.xsd`/`.wsdl` by name, OpenAPI/MCP) | Service enumeration, schema disclosure | `exposeServiceMetadata` enforced uniformly across the servlet and standalone HTTP paths and the OpenAPI/MCP generators |
| **WS-Addressing response endpoints** (`wsa:ReplyTo`, `wsa:FaultTo`) | SSRF: an inbound header names the destination of a server-initiated send | Non-anonymous response endpoints refused by default (`allowNonAnonymousResponseEndpoints`); when enabled, scheme restricted to HTTPS, destination screened at both the header-parsing and transport-selection layers, and redirects not followed |
| **OpenAPI / Swagger UI surface** | Reflected XSS from request-controlled values; Host reflected into published URLs | Host validated, values encoded for their output context, CSP with a per-response script nonce; the published `servers[].url` is relative unless `openapi.serverBaseUrl` pins it |
@@ -178,8 +178,11 @@ release 2.0.1.
**Lesson:** Any `ObjectInputStream.readObject()` on network input is a
critical-severity finding. The remaining use of Java serialization in
-Axis2 is `SafeObjectInputStream` for context externalization, which uses
-a class whitelist.
+Axis2 is `SafeObjectInputStream` for context externalization. It has no
+class allowlist and cannot have one: the data it carries includes
+application objects by design. Nothing in Axis2 feeds it from the
+network -- see item 6 below for what it does enforce, and for what an
+integrator who persists contexts has to do themselves.
### 2. XML Parsing (XXE/SSRF)
@@ -226,8 +229,32 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
block HTTP/HTTPS/FTP/JAR/file scheme resolution to prevent SSRF via
xmlschema-core's `DefaultURIResolver`.
-4. **Deserialization whitelist:** `SafeObjectInputStream` restricts Java
- object deserialization to known Axis2 context classes.
+4. **Deserialization of externalized contexts (2.0.2):** There is no class
+ allowlist, and one cannot be shipped: context externalization exists to
+ carry application objects -- `SelfManagedDataHolder` holds whatever a
+ service put there, and a `Parameter` value may be any serializable
+ object -- so a list of Axis2's own context classes would refuse the data
+ the feature is for. Earlier revisions of this document claimed such an
+ allowlist; it never existed. What holds without knowing the
+ application's types:
+
+ - The streams Axis2 creates refuse **dynamic proxies**. Axis2 never
+ writes one, and a proxy over an attacker-chosen invocation handler is
+ the entry point of the classic gadget chains. Override with
+ `org.apache.axis2.context.externalize.allowProxies=true`.
+ - `org.apache.axis2.context.externalize.serialFilter` takes a JEP 290
+ pattern applied to those streams only, for an integrator who does know
+ their own types. An unparseable pattern, or one defining no filter, is
+ an error rather than a silent no-op. The JVM-wide `jdk.serialFilter`
+ applies as well.
+ - Not covered: where the writer chose object form, `SafeObjectInputStream`
+ calls `readObject()` on the `ObjectInput` its caller supplied. That
+ stream belongs to the caller and has already read objects by then, so no
+ filter can be installed on it. Restricting it is the caller's to do.
+
+ No Axis2 code path feeds these streams from the network. The risk arrives
+ when an integrator persists or replicates contexts, and deserializing
+ attacker-influenced bytes stays dangerous whatever is configured here.
5. **Clustering removed:** The entire clustering module (Tribes-based
inter-node communication with unvalidated deserialization) has been
diff --git a/modules/kernel/src/org/apache/axis2/context/externalize/ContextDeserializationFilter.java b/modules/kernel/src/org/apache/axis2/context/externalize/ContextDeserializationFilter.java
new file mode 100644
index 0000000000..f24b8a1fcb
--- /dev/null
+++ b/modules/kernel/src/org/apache/axis2/context/externalize/ContextDeserializationFilter.java
@@ -0,0 +1,153 @@
+/*
+ * 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.context.externalize;
+
+import java.io.ObjectInputFilter;
+import java.io.ObjectInputStream;
+import java.lang.reflect.Proxy;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * The deserialization filter applied to the object streams context externalization
+ * creates.
+ * <p>
+ * <b>What this can and cannot be.</b> Axis2 cannot ship a list of permitted classes
+ * for this data. Context externalization deliberately carries application objects:
+ * {@link org.apache.axis2.util.SelfManagedDataHolder} exists to hold whatever a
+ * service put there, and a {@link org.apache.axis2.description.Parameter} value can
+ * be any serializable object. A list restricted to Axis2's own context classes would
+ * refuse the data the feature is for. So the useful properties are the ones that hold
+ * without knowing the application's types:
+ * <ul>
+ * <li>dynamic proxies are refused. Axis2's own externalization never writes one, and
+ * a proxy backed by an attacker-chosen invocation handler is the entry point of
+ * the classic gadget chains. Set
+ * {@code org.apache.axis2.context.externalize.allowProxies=true} if an
+ * application genuinely serializes a proxy into its self-managed data.</li>
+ * <li>an integrator who knows their own types can name them:
+ * {@code org.apache.axis2.context.externalize.serialFilter} takes a JEP 290
+ * pattern and is applied to these streams only, leaving the rest of the JVM
+ * alone. The JVM-wide {@code jdk.serialFilter} continues to apply as well.</li>
+ * </ul>
+ * <p>
+ * <b>What is not covered.</b> {@code SafeObjectInputStream} has a second path:
+ * where the writer chose object form, it calls {@code readObject()} on the
+ * {@code ObjectInput} its caller supplied. That stream belongs to the caller --
+ * a container persisting a session, or an application's own code -- and by the time
+ * {@code readExternal} runs it has already read objects, so a filter cannot be
+ * installed on it ({@code setObjectInputFilter} rejects that). Restricting that path
+ * is the caller's to do, with {@code jdk.serialFilter} or a filter of their own.
+ * <p>
+ * Deserializing bytes an attacker can influence remains dangerous whatever is set
+ * here. Nothing in Axis2 feeds these streams from the network; the risk arrives when
+ * an integrator persists or replicates contexts.
+ */
+final class ContextDeserializationFilter implements ObjectInputFilter {
+
+ /** A JEP 290 pattern applied to context deserialization only. */
+ static final String SERIAL_FILTER_PROPERTY =
+ "org.apache.axis2.context.externalize.serialFilter";
+
+ /** Set true to permit dynamic proxies in externalized context data. */
+ static final String ALLOW_PROXIES_PROPERTY =
+ "org.apache.axis2.context.externalize.allowProxies";
+
+ private static final Log log = LogFactory.getLog(ContextDeserializationFilter.class);
+
+ private final ObjectInputFilter configured;
+ private final boolean allowProxies;
+
+ private ContextDeserializationFilter(ObjectInputFilter configured, boolean allowProxies) {
+ this.configured = configured;
+ this.allowProxies = allowProxies;
+ }
+
+ /**
+ * Installs the filter on a stream this package created.
+ *
+ * @param stream a freshly created stream, before anything has been read from it
+ */
+ static void apply(ObjectInputStream stream) {
+ stream.setObjectInputFilter(create());
+ }
+
+ static ContextDeserializationFilter create() {
+ ObjectInputFilter configured = null;
+ String pattern = getProperty(SERIAL_FILTER_PROPERTY);
+ if (pattern != null && !pattern.trim().isEmpty()) {
+ try {
+ // NumberFormatException for a bad limit is an IllegalArgumentException.
+ configured = ObjectInputFilter.Config.createFilter(pattern.trim());
+ } catch (IllegalArgumentException e) {
+ // Fail loudly on a pattern that will not compile: whoever set one
+ // meant to restrict something, and ignoring it silently would leave
+ // them believing a restriction is in force.
+ throw new IllegalArgumentException("The value of "
+ + SERIAL_FILTER_PROPERTY + " is not a valid filter pattern", e);
+ }
+ if (configured == null) {
+ // A degenerate pattern such as ";;" compiles to no filter at all,
+ // which reads as success but restricts nothing. Same reasoning.
+ throw new IllegalArgumentException("The value of "
+ + SERIAL_FILTER_PROPERTY + " defines no filter: " + pattern);
+ }
+ }
+ return new ContextDeserializationFilter(configured,
+ Boolean.parseBoolean(getProperty(ALLOW_PROXIES_PROPERTY)));
+ }
+
+ public Status checkInput(FilterInfo filterInfo) {
+ Class<?> serialClass = filterInfo.serialClass();
+ if (serialClass == null) {
+ // A stream-size, depth or reference count check rather than a class.
+ return delegate(filterInfo);
+ }
+
+ Class<?> componentType = serialClass;
+ while (componentType.isArray()) {
+ componentType = componentType.getComponentType();
+ }
+ if (!allowProxies && Proxy.isProxyClass(componentType)) {
+ if (log.isDebugEnabled()) {
+ log.debug("Refusing a dynamic proxy in externalized context data: "
+ + componentType.getName());
+ }
+ return Status.REJECTED;
+ }
+
+ return delegate(filterInfo);
+ }
+
+ private Status delegate(FilterInfo filterInfo) {
+ if (configured != null) {
+ return configured.checkInput(filterInfo);
+ }
+ return Status.UNDECIDED;
+ }
+
+ private static String getProperty(String name) {
+ try {
+ return System.getProperty(name);
+ } catch (SecurityException e) {
+ return null;
+ }
+ }
+}
diff --git a/modules/kernel/src/org/apache/axis2/context/externalize/SafeObjectInputStream.java b/modules/kernel/src/org/apache/axis2/context/externalize/SafeObjectInputStream.java
index d2ad38745d..52c163f678 100644
--- a/modules/kernel/src/org/apache/axis2/context/externalize/SafeObjectInputStream.java
+++ b/modules/kernel/src/org/apache/axis2/context/externalize/SafeObjectInputStream.java
@@ -392,6 +392,12 @@ public class SafeObjectInputStream implements ObjectInput, ObjectStreamConstants
private ObjectInputStream createObjectInputStream(InputStream is) throws IOException {
// The created ObjectInputStream must use the same class/object resolution
// code that is used by the original ObjectInput
- return new ObjectInputStreamWithCL(is);
+ ObjectInputStreamWithCL stream = new ObjectInputStreamWithCL(is);
+ // Every byte-form read routes through here, so this is the one place a
+ // filter reaches each stream this class owns. See ContextDeserializationFilter
+ // for what it can enforce, and for why the object-form path -- readObject()
+ // on the ObjectInput the caller supplied -- is the caller's to restrict.
+ ContextDeserializationFilter.apply(stream);
+ return stream;
}
}
diff --git a/modules/kernel/test/org/apache/axis2/context/externalize/ContextDeserializationFilterTest.java b/modules/kernel/test/org/apache/axis2/context/externalize/ContextDeserializationFilterTest.java
new file mode 100644
index 0000000000..215d083545
--- /dev/null
+++ b/modules/kernel/test/org/apache/axis2/context/externalize/ContextDeserializationFilterTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.context.externalize;
+
+import java.io.ObjectInputFilter;
+import java.io.Serializable;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+
+import junit.framework.TestCase;
+
+/**
+ * Context externalization carries application objects on purpose -- self-managed
+ * data and Parameter values can be anything serializable -- so Axis2 cannot ship a
+ * list of permitted classes for it. These tests pin what the filter does enforce
+ * regardless of the application's types, and that an integrator who does know their
+ * types can restrict them.
+ */
+public class ContextDeserializationFilterTest extends TestCase {
+
+ private String savedFilter;
+ private String savedProxies;
+
+ @Override
+ protected void setUp() throws Exception {
+ savedFilter = System.getProperty(ContextDeserializationFilter.SERIAL_FILTER_PROPERTY);
+ savedProxies = System.getProperty(ContextDeserializationFilter.ALLOW_PROXIES_PROPERTY);
+ System.clearProperty(ContextDeserializationFilter.SERIAL_FILTER_PROPERTY);
+ System.clearProperty(ContextDeserializationFilter.ALLOW_PROXIES_PROPERTY);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ restore(ContextDeserializationFilter.SERIAL_FILTER_PROPERTY, savedFilter);
+ restore(ContextDeserializationFilter.ALLOW_PROXIES_PROPERTY, savedProxies);
+ }
+
+ private void restore(String name, String value) {
+ if (value == null) {
+ System.clearProperty(name);
+ } else {
+ System.setProperty(name, value);
+ }
+ }
+
+ /** The gadget-chain entry point Axis2 itself never writes. */
+ public void testDynamicProxiesAreRefused() {
+ assertEquals(ObjectInputFilter.Status.REJECTED,
+ check(ContextDeserializationFilter.create(), proxyClass()));
+ }
+
+ public void testProxyArraysAreRefusedToo() {
+ Class<?> proxyArray = java.lang.reflect.Array.newInstance(proxyClass(), 0).getClass();
+ assertEquals(ObjectInputFilter.Status.REJECTED,
+ check(ContextDeserializationFilter.create(), proxyArray));
+ }
+
+ /**
+ * Application data has to keep working: this feature exists to carry it, so an
+ * ordinary serializable class is not the filter's business.
+ */
+ public void testApplicationClassesAreNotRefused() {
+ ContextDeserializationFilter filter = ContextDeserializationFilter.create();
+ assertEquals(ObjectInputFilter.Status.UNDECIDED, check(filter, HashMap.class));
+ assertEquals(ObjectInputFilter.Status.UNDECIDED, check(filter, String.class));
+ assertEquals(ObjectInputFilter.Status.UNDECIDED,
+ check(filter, ApplicationPayload.class));
+ }
+
+ /** An escape hatch, for an application that really does serialize a proxy. */
+ public void testProxiesCanBeAllowedBack() {
+ System.setProperty(ContextDeserializationFilter.ALLOW_PROXIES_PROPERTY, "true");
+ assertEquals(ObjectInputFilter.Status.UNDECIDED,
+ check(ContextDeserializationFilter.create(), proxyClass()));
+ }
+
+ /** An integrator who knows their own types can name them. */
+ public void testAConfiguredPatternIsEnforced() {
+ System.setProperty(ContextDeserializationFilter.SERIAL_FILTER_PROPERTY,
+ "java.util.HashMap;!*");
+ ContextDeserializationFilter filter = ContextDeserializationFilter.create();
+ assertEquals(ObjectInputFilter.Status.ALLOWED, check(filter, HashMap.class));
+ assertEquals(ObjectInputFilter.Status.REJECTED,
+ check(filter, ApplicationPayload.class));
+ }
+
+ /**
+ * A pattern that will not compile must not be ignored: whoever set it believes
+ * something is restricted.
+ */
+ public void testAnUnparseablePatternIsRefusedLoudly() {
+ // An unknown limit, and a bad limit value: the second throws
+ // NumberFormatException, which is an IllegalArgumentException.
+ for (String bad : new String[] {"bogus=1", "maxdepth=abc", "maxdepth=-5"}) {
+ System.setProperty(ContextDeserializationFilter.SERIAL_FILTER_PROPERTY, bad);
+ try {
+ ContextDeserializationFilter.create();
+ fail("an invalid filter pattern must not be silently ignored: " + bad);
+ } catch (IllegalArgumentException expected) {
+ assertTrue(expected.getMessage()
+ .contains(ContextDeserializationFilter.SERIAL_FILTER_PROPERTY));
+ }
+ }
+ }
+
+ /**
+ * A pattern that compiles to nothing reads as success while restricting nothing,
+ * which is the same trap as one that will not compile.
+ */
+ public void testAPatternDefiningNoFilterIsRefusedLoudly() {
+ System.setProperty(ContextDeserializationFilter.SERIAL_FILTER_PROPERTY, ";;");
+ try {
+ ContextDeserializationFilter.create();
+ fail("a pattern defining no filter must not pass for a restriction");
+ } catch (IllegalArgumentException expected) {
+ assertTrue(expected.getMessage().contains("defines no filter"));
+ }
+ }
+
+ private ObjectInputFilter.Status check(ObjectInputFilter filter, Class<?> serialClass) {
+ return filter.checkInput(new StubFilterInfo(serialClass));
+ }
+
+ private Class<?> proxyClass() {
+ return Proxy.getProxyClass(getClass().getClassLoader(), new Class[] {Runnable.class});
+ }
+
+ /** A stand-in for an application object in self-managed data. */
+ private static class ApplicationPayload implements Serializable {
+ private static final long serialVersionUID = 1L;
+ }
+
+ /** Reports one class; the counters are what a stream would supply. */
+ private static class StubFilterInfo implements ObjectInputFilter.FilterInfo {
+ private final Class<?> serialClass;
+
+ StubFilterInfo(Class<?> serialClass) {
+ this.serialClass = serialClass;
+ }
+
+ public Class<?> serialClass() {
+ return serialClass;
+ }
+
+ public long arrayLength() {
+ return -1;
+ }
+
+ public long depth() {
+ return 1;
+ }
+
+ public long references() {
+ return 1;
+ }
+
+ public long streamBytes() {
+ return 1;
+ }
+ }
+
+}
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index 5fce9a269b..b1149f214a 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -110,6 +110,19 @@ in `SECURITY.md`.
resolves schemas over the network passes `-asl` to the CLI or sets
`<allowAbsoluteSchemaLocations>true</allowAbsoluteSchemaLocations>` on the plugin.
+- **Context deserialization: a documented allowlist that never existed is now
+ described accurately, and what can be enforced is.** `SECURITY.md` claimed three
+ times that `SafeObjectInputStream` restricts deserialization to known Axis2
+ context classes. It does not, and cannot: the feature exists to carry application
+ objects -- `SelfManagedDataHolder` holds whatever a service put there, and a
+ `Parameter` value may be any serializable object -- so such a list would refuse
+ the data it is for. The streams Axis2 creates now refuse **dynamic proxies**
+ (`org.apache.axis2.context.externalize.allowProxies=true` to override), and
+ `org.apache.axis2.context.externalize.serialFilter` accepts a JEP 290 pattern for
+ an integrator who does know their own types. No Axis2 path feeds these streams
+ from the network; if you persist or replicate contexts, restrict the stream you
+ own.
+
- **OpenAPI and Swagger UI output.** Request-controlled values are validated and
encoded for the context they are written into, the served page carries a
Content-Security-Policy with a per-response script nonce, and the published
(axis-axis2-java-core) 08/13: Make two threat-model claims true, and scope what they do not cover
Posted by ro...@apache.org.
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 3ff914a61448b50c620e8e41a2c48980f23158b4
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Fri Sep 4 16:12:01 2026 -1000
Make two threat-model claims true, and scope what they do not cover
SECURITY.md said every parser factory the framework creates disables DTDs and
external entities, and that the deployment resolvers block file:. Neither held.
The SAAJ and JAX-WS conversions now use the hardened factory, and file: is
refused by the resolvers -- reachable in one of the four, where an absolute
file: import passed the guard. The document now names what stays uncovered:
build-time codegen factories, the deliberate hbm.xml DTD exception, and the
TransformerFactory instances.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
SECURITY.md | 35 ++++++++++++++++++---
.../axis2/jaxws/handler/LogicalMessageImpl.java | 7 +++--
.../deployment/resolver/AARBasedWSDLLocator.java | 9 +++++-
.../resolver/AARFileBasedURIResolver.java | 9 +++++-
.../deployment/resolver/WarBasedWSDLLocator.java | 9 +++++-
.../resolver/WarFileBasedURIResolver.java | 9 +++++-
.../kernel/src/org/apache/axis2/util/XMLUtils.java | 5 +--
.../apache/axis2/deployment/URIResolverTest.java | 36 ++++++++++++++++++++++
.../description/builder/JAXWSRIWSDLGenerator.java | 4 +--
.../src/org/apache/axis2/saaj/SOAPBodyImpl.java | 6 ++--
.../src/org/apache/axis2/saaj/util/SAAJUtil.java | 6 ++--
src/site/markdown/release-notes/2.0.2.md | 10 ++++++
12 files changed, 125 insertions(+), 20 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 7d0f123750..41c3f93a79 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -216,9 +216,25 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
## Existing Security Hardening
-1. **XML parsing:** All `DocumentBuilderFactory` and `SAXParserFactory`
- instances created by the framework disable DTDs and external entities
- (`XMLUtils.java`, `SecureWSDLLocator.java`, `DefaultEntityResolver.java`).
+1. **XML parsing (scoped accurately in 2.0.2):** The parser factories on the
+ runtime paths disable DTDs and external entities:
+ `XMLUtils.newSecureDocumentBuilderFactory` and `XMLUtils.newDocument`,
+ `SecureWSDLLocator`, `HardenedWSDLLocator`, `DefaultEntityResolver`, and the
+ SAAJ and JAX-WS conversions that were missed until 2.0.2 (`XMLUtils.toDOM`,
+ `SAAJUtil.toDOM`, `SOAPBodyImpl`, `LogicalMessageImpl`,
+ `JAXWSRIWSDLGenerator`).
+
+ Earlier revisions of this document said *all* factories created by the
+ framework. That was not true and is not a promise to make:
+
+ - The code-generation tooling creates its own factories. Its dedicated
+ entry points are hardened (see item 13 and `XSD2Java`), but the
+ build-time emitters and schema generators are not audited here.
+ - `HbmXmlIntrospector` allows DTDs deliberately, because hbm.xml files
+ declare one. That is a permanent exception, not a gap.
+ - `TransformerFactory` instances do not set `ACCESS_EXTERNAL_DTD` or
+ `ACCESS_EXTERNAL_STYLESHEET`. Restricting them can break stylesheets
+ that legitimately import, so it has not been done blind.
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
@@ -243,8 +259,17 @@ migration from `commons-fileupload` 1.x to `commons-fileupload2` in
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
- xmlschema-core's `DefaultURIResolver`.
+ refuse HTTP/HTTPS/FTP/JAR/file scheme resolution, so an import cannot
+ reach the network or the server's filesystem through xmlschema-core's
+ `DefaultURIResolver`; a location that is not absolute is looked up inside
+ the archive.
+
+ `file:` was added to the guard in 2.0.2. Only `AARBasedWSDLLocator` had
+ been letting one through: it counts `file:` as absolute, so an absolute
+ `file:` import passed the scheme check and fell through to the parent
+ resolver. In the other three the predicate does not count `file:` as
+ absolute, so such a location was already treated as an archive resource
+ name and never opened a filesystem path.
4. **Deserialization of externalized contexts (2.0.2):** There is no class
allowlist, and one cannot be shipped: context externalization exists to
diff --git a/modules/jaxws/src/org/apache/axis2/jaxws/handler/LogicalMessageImpl.java b/modules/jaxws/src/org/apache/axis2/jaxws/handler/LogicalMessageImpl.java
index fc831aa89e..023ce9102c 100644
--- a/modules/jaxws/src/org/apache/axis2/jaxws/handler/LogicalMessageImpl.java
+++ b/modules/jaxws/src/org/apache/axis2/jaxws/handler/LogicalMessageImpl.java
@@ -21,6 +21,7 @@ package org.apache.axis2.jaxws.handler;
import org.apache.axiom.om.util.StAXUtils;
import org.apache.axiom.soap.SOAPEnvelope;
+import org.apache.axis2.util.XMLUtils;
import org.apache.axis2.jaxws.ExceptionFactory;
import org.apache.axis2.jaxws.message.Block;
import org.apache.axis2.jaxws.message.Message;
@@ -212,9 +213,9 @@ public class LogicalMessageImpl implements LogicalMessage {
// DOMSource so that the handler programmer can read the data
// multiple times and (as opposed to using a StreamSource) and
// they can more easily access the data in DOM form.
- DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
- dbf.setNamespaceAware(true);
-
+ DocumentBuilderFactory dbf =
+ XMLUtils.newSecureDocumentBuilderFactory();
+
DocumentBuilder db = dbf.newDocumentBuilder();
Document dom = db.parse(bais);
payloads.HANDLER_PAYLOAD = new DOMSource(dom);
diff --git a/modules/kernel/src/org/apache/axis2/deployment/resolver/AARBasedWSDLLocator.java b/modules/kernel/src/org/apache/axis2/deployment/resolver/AARBasedWSDLLocator.java
index c3e4b367ad..e76b539fff 100644
--- a/modules/kernel/src/org/apache/axis2/deployment/resolver/AARBasedWSDLLocator.java
+++ b/modules/kernel/src/org/apache/axis2/deployment/resolver/AARBasedWSDLLocator.java
@@ -77,7 +77,14 @@ public class AARBasedWSDLLocator extends DefaultURIResolver implements WSDLLocat
if (loc.regionMatches(true, 0, "http:", 0, 5)
|| loc.regionMatches(true, 0, "https:", 0, 6)
|| loc.regionMatches(true, 0, "ftp:", 0, 4)
- || loc.regionMatches(true, 0, "jar:", 0, 4)) {
+ || loc.regionMatches(true, 0, "jar:", 0, 4)
+ // file: as well. The threat model says these resolvers block it,
+ // and an absolute file: import would read the server's own
+ // filesystem rather than the archive these resolvers exist to read
+ // from. Only AARBasedWSDLLocator counts file: as absolute, so only
+ // there did one reach this branch and fall through to the parent
+ // resolver; the rest are covered so the guard reads the same way.
+ || loc.regionMatches(true, 0, "file:", 0, 5)) {
throw new RuntimeException(
"Remote WSDL import blocked: " + loc);
}
diff --git a/modules/kernel/src/org/apache/axis2/deployment/resolver/AARFileBasedURIResolver.java b/modules/kernel/src/org/apache/axis2/deployment/resolver/AARFileBasedURIResolver.java
index 801a33a97b..e4136b0431 100644
--- a/modules/kernel/src/org/apache/axis2/deployment/resolver/AARFileBasedURIResolver.java
+++ b/modules/kernel/src/org/apache/axis2/deployment/resolver/AARFileBasedURIResolver.java
@@ -64,7 +64,14 @@ public class AARFileBasedURIResolver extends DefaultURIResolver {
if (loc.regionMatches(true, 0, "http:", 0, 5)
|| loc.regionMatches(true, 0, "https:", 0, 6)
|| loc.regionMatches(true, 0, "ftp:", 0, 4)
- || loc.regionMatches(true, 0, "jar:", 0, 4)) {
+ || loc.regionMatches(true, 0, "jar:", 0, 4)
+ // file: as well. The threat model says these resolvers block it,
+ // and an absolute file: import would read the server's own
+ // filesystem rather than the archive these resolvers exist to read
+ // from. Only AARBasedWSDLLocator counts file: as absolute, so only
+ // there did one reach this branch and fall through to the parent
+ // resolver; the rest are covered so the guard reads the same way.
+ || loc.regionMatches(true, 0, "file:", 0, 5)) {
log.warn("Blocked remote schema resolution in AAR deployment: " + loc);
return new InputSource(new java.io.ByteArrayInputStream(new byte[0]));
}
diff --git a/modules/kernel/src/org/apache/axis2/deployment/resolver/WarBasedWSDLLocator.java b/modules/kernel/src/org/apache/axis2/deployment/resolver/WarBasedWSDLLocator.java
index db8c8274e3..ba37dc26f9 100644
--- a/modules/kernel/src/org/apache/axis2/deployment/resolver/WarBasedWSDLLocator.java
+++ b/modules/kernel/src/org/apache/axis2/deployment/resolver/WarBasedWSDLLocator.java
@@ -64,7 +64,14 @@ public class WarBasedWSDLLocator extends DefaultURIResolver implements WSDLLocat
if (loc.regionMatches(true, 0, "http:", 0, 5)
|| loc.regionMatches(true, 0, "https:", 0, 6)
|| loc.regionMatches(true, 0, "ftp:", 0, 4)
- || loc.regionMatches(true, 0, "jar:", 0, 4)) {
+ || loc.regionMatches(true, 0, "jar:", 0, 4)
+ // file: as well. The threat model says these resolvers block it,
+ // and an absolute file: import would read the server's own
+ // filesystem rather than the archive these resolvers exist to read
+ // from. Only AARBasedWSDLLocator counts file: as absolute, so only
+ // there did one reach this branch and fall through to the parent
+ // resolver; the rest are covered so the guard reads the same way.
+ || loc.regionMatches(true, 0, "file:", 0, 5)) {
throw new RuntimeException(
"Remote WSDL import blocked: " + loc);
}
diff --git a/modules/kernel/src/org/apache/axis2/deployment/resolver/WarFileBasedURIResolver.java b/modules/kernel/src/org/apache/axis2/deployment/resolver/WarFileBasedURIResolver.java
index a19911db64..6298fd87cf 100644
--- a/modules/kernel/src/org/apache/axis2/deployment/resolver/WarFileBasedURIResolver.java
+++ b/modules/kernel/src/org/apache/axis2/deployment/resolver/WarFileBasedURIResolver.java
@@ -58,7 +58,14 @@ public class WarFileBasedURIResolver extends DefaultURIResolver {
if (resolved.regionMatches(true, 0, "http:", 0, 5)
|| resolved.regionMatches(true, 0, "https:", 0, 6)
|| resolved.regionMatches(true, 0, "ftp:", 0, 4)
- || resolved.regionMatches(true, 0, "jar:", 0, 4)) {
+ || resolved.regionMatches(true, 0, "jar:", 0, 4)
+ // file: as well. The threat model says these resolvers block it,
+ // and an absolute file: import would read the server's own
+ // filesystem rather than the archive these resolvers exist to read
+ // from. Only AARBasedWSDLLocator counts file: as absolute, so only
+ // there did one reach this branch and fall through to the parent
+ // resolver; the rest are covered so the guard reads the same way.
+ || resolved.regionMatches(true, 0, "file:", 0, 5)) {
log.warn("Blocked remote schema resolution in WAR deployment: " + resolved);
return new InputSource(new java.io.ByteArrayInputStream(new byte[0]));
}
diff --git a/modules/kernel/src/org/apache/axis2/util/XMLUtils.java b/modules/kernel/src/org/apache/axis2/util/XMLUtils.java
index 61911463ea..b30ace36c3 100644
--- a/modules/kernel/src/org/apache/axis2/util/XMLUtils.java
+++ b/modules/kernel/src/org/apache/axis2/util/XMLUtils.java
@@ -554,8 +554,9 @@ public class XMLUtils {
element.serialize(baos);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
+ // This file is the one SECURITY.md item 1 cites for parser hardening, and
+ // this method sat eight lines below the hardened factory without using it.
+ DocumentBuilderFactory factory = newSecureDocumentBuilderFactory();
return factory.newDocumentBuilder().parse(bais).getDocumentElement();
}
diff --git a/modules/kernel/test/org/apache/axis2/deployment/URIResolverTest.java b/modules/kernel/test/org/apache/axis2/deployment/URIResolverTest.java
index 29bd5c1e87..ac20ca4f8f 100644
--- a/modules/kernel/test/org/apache/axis2/deployment/URIResolverTest.java
+++ b/modules/kernel/test/org/apache/axis2/deployment/URIResolverTest.java
@@ -21,6 +21,7 @@ package org.apache.axis2.deployment;
import junit.framework.TestCase;
+import org.apache.axis2.deployment.resolver.AARBasedWSDLLocator;
import org.apache.axis2.deployment.resolver.AARFileBasedURIResolver;
import org.apache.axis2.deployment.resolver.WarFileBasedURIResolver;
import org.xml.sax.InputSource;
@@ -119,4 +120,39 @@ public class URIResolverTest extends TestCase {
return isAbsolute(uri);
}
}
+
+ /**
+ * The threat model says these resolvers block file: resolution. Only one of the
+ * four ever let a file: location reach the scheme guard: AARBasedWSDLLocator
+ * counts file: as absolute, so an absolute file: import passed the guard, fell
+ * through to the parent resolver and read the server's filesystem instead of the
+ * archive. That is the one this closes.
+ */
+ public void testAarWsdlLocatorRefusesAFileImport() {
+ try {
+ new AARBasedWSDLLocator(null, null, null)
+ .getImportInputSource("file:/deploy/service.wsdl", "/etc/passwd");
+ fail("an absolute file: import must not be resolved from the filesystem");
+ } catch (RuntimeException expected) {
+ assertTrue("should say it was blocked, was: " + expected.getMessage(),
+ expected.getMessage().contains("blocked"));
+ }
+ }
+
+ /**
+ * The other three reach safety a different way, and it is worth pinning which:
+ * their isAbsolute does not count file: as absolute, so the location is treated
+ * as a name to look up inside the archive and never opens a filesystem path. The
+ * file: clause added to their scheme guard is therefore unreachable today, and
+ * kept only so the guard reads the same way in all four.
+ */
+ public void testFileLocationsAreLookedUpInTheArchiveNotTheFilesystem() {
+ WarFileBasedURIResolver war =
+ new WarFileBasedURIResolver(getClass().getClassLoader());
+ InputSource inputSource = war.resolveEntity(null,
+ "file:///etc/passwd", "file:///etc/passwd");
+ assertNotNull(inputSource);
+ assertNull("nothing may be read from the filesystem",
+ inputSource.getByteStream());
+ }
}
diff --git a/modules/metadata/src/org/apache/axis2/jaxws/description/builder/JAXWSRIWSDLGenerator.java b/modules/metadata/src/org/apache/axis2/jaxws/description/builder/JAXWSRIWSDLGenerator.java
index fcb53a01e9..f23c3e4711 100644
--- a/modules/metadata/src/org/apache/axis2/jaxws/description/builder/JAXWSRIWSDLGenerator.java
+++ b/modules/metadata/src/org/apache/axis2/jaxws/description/builder/JAXWSRIWSDLGenerator.java
@@ -20,6 +20,7 @@
package org.apache.axis2.jaxws.description.builder;
import org.apache.axiom.util.UIDGenerator;
+import org.apache.axis2.util.XMLUtils;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.dataretrieval.SchemaSupplier;
@@ -325,8 +326,7 @@ public class JAXWSRIWSDLGenerator implements SchemaSupplier, WSDLSupplier {
HashMap<String, XmlSchema> docMap = new HashMap<String, XmlSchema>();
// Doc factory to read schema files
- DocumentBuilderFactory fac = DocumentBuilderFactory.newInstance();
- fac.setNamespaceAware(true);
+ DocumentBuilderFactory fac = XMLUtils.newSecureDocumentBuilderFactory();
List<File> schemaFiles = getSchemaFiles(localOutputDirectory);
for (File schemaFile : schemaFiles) {
diff --git a/modules/saaj/src/org/apache/axis2/saaj/SOAPBodyImpl.java b/modules/saaj/src/org/apache/axis2/saaj/SOAPBodyImpl.java
index d0b60487cc..567dd45cff 100644
--- a/modules/saaj/src/org/apache/axis2/saaj/SOAPBodyImpl.java
+++ b/modules/saaj/src/org/apache/axis2/saaj/SOAPBodyImpl.java
@@ -19,6 +19,7 @@
package org.apache.axis2.saaj;
+import org.apache.axis2.util.XMLUtils;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.OMNode;
@@ -457,10 +458,11 @@ public class SOAPBodyImpl extends SOAPElementImpl<org.apache.axiom.soap.SOAPBody
Document document;
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
try {
+ // Created inside the try: the hardened factory reports a parser that
+ // will not accept the restrictions, which this block already handles.
+ DocumentBuilderFactory factory = XMLUtils.newSecureDocumentBuilderFactory();
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.newDocument();
Element element =
diff --git a/modules/saaj/src/org/apache/axis2/saaj/util/SAAJUtil.java b/modules/saaj/src/org/apache/axis2/saaj/util/SAAJUtil.java
index 20ffe23aef..bfef22ce4d 100644
--- a/modules/saaj/src/org/apache/axis2/saaj/util/SAAJUtil.java
+++ b/modules/saaj/src/org/apache/axis2/saaj/util/SAAJUtil.java
@@ -19,6 +19,7 @@
package org.apache.axis2.saaj.util;
+import org.apache.axis2.util.XMLUtils;
import org.apache.axiom.blob.Blob;
import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMAttachmentAccessor;
@@ -128,8 +129,9 @@ public class SAAJUtil {
element.serialize(baos);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
- DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
- factory.setNamespaceAware(true);
+ // SECURITY.md item 1 says every framework parser factory refuses DTDs and
+ // external entities; this was one of the sites that did not.
+ DocumentBuilderFactory factory = XMLUtils.newSecureDocumentBuilderFactory();
return factory.newDocumentBuilder().parse(bais).getDocumentElement();
}
diff --git a/src/site/markdown/release-notes/2.0.2.md b/src/site/markdown/release-notes/2.0.2.md
index 2a2e0f835e..905e9210c1 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -164,6 +164,16 @@ 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.
+- **Two threat-model claims made true, and their limits stated.** The document said
+ every parser factory the framework creates disables DTDs and external entities, and
+ that the AAR/WAR resolvers block `file:` resolution. Neither held. The SAAJ and
+ JAX-WS conversions now use the hardened factory (`XMLUtils.toDOM`,
+ `SAAJUtil.toDOM`, `SOAPBodyImpl`, `LogicalMessageImpl`, `JAXWSRIWSDLGenerator`), and
+ `file:` is refused by the deployment resolvers -- which mattered in one of the four,
+ where an absolute `file:` import had been reaching the filesystem. `SECURITY.md` now
+ also names what is *not* covered: the build-time codegen factories, the deliberate
+ hbm.xml DTD exception, and the `TransformerFactory` instances.
+
- **OpenAPI and Swagger UI output.** Request-controlled values are validated and
encoded for the context they are written into, the served page carries a
Content-Security-Policy with a per-response script nonce, and the published