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 <[email protected]> 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) <[email protected]> --- 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.
