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
The following commit(s) were added to refs/heads/master by this push:
new aea2671e3c Bound the JSON request body in every JSON builder
aea2671e3c is described below
commit aea2671e3c5ebd8638d37db30d69639223366d5d
Author: Robert Lazarski <[email protected]>
AuthorDate: Tue Sep 29 12:08:45 2026 -1000
Bound the JSON request body in every JSON builder
The request-body ceilings never reached axis2-json, so an application/json
POST was read without limit. Add jsonMaxRequestSize (100 MB, -1 to opt out)
and apply it in the legacy, Gson, Moshi and HTTP/2 JSON builders alike: a
streaming reader still materialises a single string token whole.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
SECURITY.md | 9 +-
.../apache/axis2/json/AbstractJSONDataSource.java | 4 +-
.../apache/axis2/json/AbstractJSONOMBuilder.java | 10 +-
.../org/apache/axis2/json/gson/JsonBuilder.java | 5 +
.../axis2/json/gsonh2/EnhancedGsonJsonBuilder.java | 7 +
.../org/apache/axis2/json/moshi/JsonBuilder.java | 5 +
.../json/moshih2/EnhancedMoshiJsonBuilder.java | 7 +
.../axis2/json/JsonRequestSizeLimitTest.java | 163 +++++++++++++++++++++
.../apache/axis2/builder/RequestSizeLimits.java | 18 ++-
src/site/markdown/release-notes/2.0.2.md | 5 +-
10 files changed, 224 insertions(+), 9 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index bea9d04b79..4303c1d091 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -132,7 +132,7 @@ Axis2 exposes the following URL patterns from the servlet
mapping:
|-----------|---------|-------------|
| **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 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 parser** (Gson) | Deep nesting stack exhaustion, large payload DoS |
Fuzz-tested (1.7M+ iterations); Gson nesting limits; `jsonMaxRequestSize`
bounds the body for every JSON builder |
| **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 |
| **Form-urlencoded builder** | Unbounded body read into an in-memory map |
`formUrlEncodedMaxRequestSize` bounds the read; the stream fails rather than
truncating |
@@ -404,9 +404,10 @@ migration from `commons-fileupload` 1.x to
`commons-fileupload2` in
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.
+ whole `multipart/related` body, so MTOM and SwA), `soapMaxRequestSize`
+ (100 MB, a plain SOAP or POX body) and `jsonMaxRequestSize` (100 MB, every
+ builder in `axis2-json`, streaming or not) 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
diff --git a/modules/json/src/org/apache/axis2/json/AbstractJSONDataSource.java
b/modules/json/src/org/apache/axis2/json/AbstractJSONDataSource.java
index 6d22c8236d..ccdeaf48d4 100755
--- a/modules/json/src/org/apache/axis2/json/AbstractJSONDataSource.java
+++ b/modules/json/src/org/apache/axis2/json/AbstractJSONDataSource.java
@@ -78,7 +78,9 @@ public abstract class AbstractJSONDataSource extends
AbstractPullOMDataSource {
}
jsonString = sb.toString();
} catch (IOException e) {
- throw new OMException();
+ // Keep the cause: an over-sized body surfaces here, as the
+ // ceiling applied by AbstractJSONOMBuilder.
+ throw new OMException(e);
}
isRead = true;
return jsonString;
diff --git a/modules/json/src/org/apache/axis2/json/AbstractJSONOMBuilder.java
b/modules/json/src/org/apache/axis2/json/AbstractJSONOMBuilder.java
index 8ce2c1f867..89309c7210 100644
--- a/modules/json/src/org/apache/axis2/json/AbstractJSONOMBuilder.java
+++ b/modules/json/src/org/apache/axis2/json/AbstractJSONOMBuilder.java
@@ -25,7 +25,9 @@ import org.apache.axiom.om.OMFactory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.EndpointReference;
+import org.apache.axis2.builder.BoundedInputStream;
import org.apache.axis2.builder.Builder;
+import org.apache.axis2.builder.RequestSizeLimits;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.kernel.http.util.URIEncoderDecoder;
@@ -111,8 +113,14 @@ public abstract class AbstractJSONOMBuilder implements
Builder {
if (charSetEncoding == null) {
charSetEncoding = MessageContext.DEFAULT_CHAR_SET_ENCODING;
}
+ // The data source buffers the whole body into one String, so bound
+ // the transport stream as SOAPBuilder does.
+ long maxRequestSize = RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.JSON_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_JSON_MAX_REQUEST_SIZE);
try {
- reader = new InputStreamReader(inputStream, charSetEncoding);
+ reader = new InputStreamReader(
+ BoundedInputStream.wrap(inputStream, maxRequestSize),
charSetEncoding);
} catch (UnsupportedEncodingException ex) {
throw AxisFault.makeFault(ex);
}
diff --git a/modules/json/src/org/apache/axis2/json/gson/JsonBuilder.java
b/modules/json/src/org/apache/axis2/json/gson/JsonBuilder.java
index 022f5be970..7ce3215df6 100644
--- a/modules/json/src/org/apache/axis2/json/gson/JsonBuilder.java
+++ b/modules/json/src/org/apache/axis2/json/gson/JsonBuilder.java
@@ -25,7 +25,9 @@ import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
+import org.apache.axis2.builder.BoundedInputStream;
import org.apache.axis2.builder.Builder;
+import org.apache.axis2.builder.RequestSizeLimits;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.json.factory.JsonConstant;
import org.apache.commons.logging.Log;
@@ -42,6 +44,9 @@ public class JsonBuilder implements Builder {
JsonReader jsonReader;
String charSetEncoding=null;
if (inputStream != null) {
+ inputStream = BoundedInputStream.wrap(inputStream,
RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.JSON_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_JSON_MAX_REQUEST_SIZE));
try {
charSetEncoding = (String)
messageContext.getProperty(Constants.Configuration.CHARACTER_SET_ENCODING);
jsonReader = new JsonReader(new InputStreamReader(inputStream,
charSetEncoding));
diff --git
a/modules/json/src/org/apache/axis2/json/gsonh2/EnhancedGsonJsonBuilder.java
b/modules/json/src/org/apache/axis2/json/gsonh2/EnhancedGsonJsonBuilder.java
index ebf7e6d7b1..3be985288a 100644
--- a/modules/json/src/org/apache/axis2/json/gsonh2/EnhancedGsonJsonBuilder.java
+++ b/modules/json/src/org/apache/axis2/json/gsonh2/EnhancedGsonJsonBuilder.java
@@ -24,7 +24,9 @@ import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
+import org.apache.axis2.builder.BoundedInputStream;
import org.apache.axis2.builder.Builder;
+import org.apache.axis2.builder.RequestSizeLimits;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.engine.AxisConfiguration;
@@ -161,6 +163,11 @@ public class EnhancedGsonJsonBuilder implements Builder {
return createDefaultEnvelope();
}
+ // The payload-size strategy below only picks a code path; it
bounds nothing.
+ inputStream = BoundedInputStream.wrap(inputStream,
RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.JSON_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_JSON_MAX_REQUEST_SIZE));
+
// Determine processing strategy based on payload characteristics
(from HTTP/2 analysis)
ProcessingStrategy strategy =
analyzeProcessingStrategy(messageContext, contentType);
diff --git a/modules/json/src/org/apache/axis2/json/moshi/JsonBuilder.java
b/modules/json/src/org/apache/axis2/json/moshi/JsonBuilder.java
index 09976cf110..2aab6202f6 100644
--- a/modules/json/src/org/apache/axis2/json/moshi/JsonBuilder.java
+++ b/modules/json/src/org/apache/axis2/json/moshi/JsonBuilder.java
@@ -29,7 +29,9 @@ import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
+import org.apache.axis2.builder.BoundedInputStream;
import org.apache.axis2.builder.Builder;
+import org.apache.axis2.builder.RequestSizeLimits;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.json.factory.JsonConstant;
import org.apache.commons.logging.Log;
@@ -46,6 +48,9 @@ public class JsonBuilder implements Builder {
JsonReader jsonReader;
String charSetEncoding=null;
if (inputStream != null) {
+ inputStream = BoundedInputStream.wrap(inputStream,
RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.JSON_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_JSON_MAX_REQUEST_SIZE));
try {
charSetEncoding = (String)
messageContext.getProperty(Constants.Configuration.CHARACTER_SET_ENCODING);
if (charSetEncoding != null &&
charSetEncoding.indexOf("UTF-8") == -1) {
diff --git
a/modules/json/src/org/apache/axis2/json/moshih2/EnhancedMoshiJsonBuilder.java
b/modules/json/src/org/apache/axis2/json/moshih2/EnhancedMoshiJsonBuilder.java
index 5db126ae50..59d90e80ab 100644
---
a/modules/json/src/org/apache/axis2/json/moshih2/EnhancedMoshiJsonBuilder.java
+++
b/modules/json/src/org/apache/axis2/json/moshih2/EnhancedMoshiJsonBuilder.java
@@ -26,7 +26,9 @@ import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAPFactory;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
+import org.apache.axis2.builder.BoundedInputStream;
import org.apache.axis2.builder.Builder;
+import org.apache.axis2.builder.RequestSizeLimits;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.engine.AxisConfiguration;
@@ -161,6 +163,11 @@ public class EnhancedMoshiJsonBuilder implements Builder {
return createDefaultEnvelope();
}
+ // The payload-size strategy below only picks a code path; it
bounds nothing.
+ inputStream = BoundedInputStream.wrap(inputStream,
RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.JSON_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_JSON_MAX_REQUEST_SIZE));
+
// Determine processing strategy based on payload characteristics
(from HTTP/2 analysis)
ProcessingStrategy strategy =
analyzeProcessingStrategy(messageContext, contentType);
diff --git
a/modules/json/test/org/apache/axis2/json/JsonRequestSizeLimitTest.java
b/modules/json/test/org/apache/axis2/json/JsonRequestSizeLimitTest.java
new file mode 100644
index 0000000000..65ea648695
--- /dev/null
+++ b/modules/json/test/org/apache/axis2/json/JsonRequestSizeLimitTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.json;
+
+import junit.framework.TestCase;
+import org.apache.axiom.om.OMSourcedElement;
+import org.apache.axis2.builder.Builder;
+import org.apache.axis2.builder.RequestSizeLimits;
+import org.apache.axis2.context.ConfigurationContext;
+import org.apache.axis2.context.MessageContext;
+import org.apache.axis2.description.Parameter;
+import org.apache.axis2.engine.AxisConfiguration;
+import org.apache.axis2.json.factory.JsonConstant;
+import org.apache.axis2.json.gson.GsonXMLStreamReader;
+import org.apache.axis2.json.gsonh2.EnhancedGsonJsonBuilder;
+import org.apache.axis2.json.moshi.MoshiXMLStreamReader;
+import org.apache.axis2.json.moshih2.EnhancedMoshiJsonBuilder;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * Every JSON builder must apply {@code jsonMaxRequestSize}. Streaming builders
+ * are no exception: one string token is still read into memory whole, which is
+ * what the body below is.
+ */
+public class JsonRequestSizeLimitTest extends TestCase {
+
+ private static final String LIMIT = "64";
+
+ private AxisConfiguration axisConfiguration;
+ private MessageContext messageContext;
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ axisConfiguration = new AxisConfiguration();
+ ConfigurationContext configurationContext = new
ConfigurationContext(axisConfiguration);
+ messageContext = configurationContext.createMessageContext();
+
messageContext.setProperty(org.apache.axis2.Constants.Configuration.CHARACTER_SET_ENCODING,
"UTF-8");
+ }
+
+ private void setLimit(String value) throws Exception {
+ axisConfiguration.addParameter(new
Parameter(RequestSizeLimits.JSON_MAX_REQUEST_SIZE, value));
+ }
+
+ private static InputStream body(int stringLength) throws Exception {
+ StringBuilder sb = new StringBuilder("{\"a\":\"");
+ for (int i = 0; i < stringLength; i++) {
+ sb.append('A');
+ }
+ return new
ByteArrayInputStream(sb.append("\"}").toString().getBytes("UTF-8"));
+ }
+
+ private static void assertExceedsCeiling(Throwable t) {
+ for (Throwable c = t; c != null; c = c.getCause()) {
+ if (c.getMessage() != null && c.getMessage().contains("exceeds the
configured maximum")) {
+ return;
+ }
+ }
+ fail("Expected the request-size ceiling to be hit, got " + t);
+ }
+
+ public void testJsonCeilingHasADefault() {
+ assertEquals(RequestSizeLimits.DEFAULT_JSON_MAX_REQUEST_SIZE,
+ RequestSizeLimits.resolve(messageContext,
+ RequestSizeLimits.JSON_MAX_REQUEST_SIZE,
+ RequestSizeLimits.DEFAULT_JSON_MAX_REQUEST_SIZE));
+ }
+
+ public void testJsonOMBuilderIsBounded() throws Exception {
+ setLimit(LIMIT);
+ try {
+ readJsonString(1000);
+ fail("An over-sized body was read in full");
+ } catch (Exception e) {
+ assertExceedsCeiling(e);
+ }
+ }
+
+ public void testJsonOMBuilderAcceptsABodyUnderTheCeiling() throws
Exception {
+ setLimit(LIMIT);
+ assertEquals("{\"a\":\"AAAAAAAA\"}", readJsonString(8));
+ }
+
+ public void testJsonOMBuilderCeilingCanBeOptedOut() throws Exception {
+ setLimit("-1");
+ assertEquals(1000 + 8, readJsonString(1000).length());
+ }
+
+ /** Builds with JSONOMBuilder, then reads the body the way its data source
does. */
+ private String readJsonString(int stringLength) throws Exception {
+ OMSourcedElement element = (OMSourcedElement) new
JSONOMBuilder().processDocument(
+ body(stringLength), JSONTestConstants.CONTENT_TYPE_MAPPED,
messageContext);
+ return (String) ((AbstractJSONDataSource)
element.getDataSource()).getObject();
+ }
+
+ public void testGsonJsonBuilderIsBounded() throws Exception {
+ setLimit(LIMIT);
+ assertGsonReaderBounded(new org.apache.axis2.json.gson.JsonBuilder());
+ }
+
+ public void testEnhancedGsonJsonBuilderIsBounded() throws Exception {
+ setLimit(LIMIT);
+ assertGsonReaderBounded(new EnhancedGsonJsonBuilder());
+ }
+
+ public void testMoshiJsonBuilderIsBounded() throws Exception {
+ setLimit(LIMIT);
+ assertMoshiReaderBounded(new
org.apache.axis2.json.moshi.JsonBuilder());
+ }
+
+ public void testEnhancedMoshiJsonBuilderIsBounded() throws Exception {
+ setLimit(LIMIT);
+ assertMoshiReaderBounded(new EnhancedMoshiJsonBuilder());
+ }
+
+ private void assertGsonReaderBounded(Builder builder) throws Exception {
+ builder.processDocument(body(1000), "application/json",
messageContext);
+ GsonXMLStreamReader reader = (GsonXMLStreamReader)
+
messageContext.getProperty(JsonConstant.GSON_XML_STREAM_READER);
+ com.google.gson.stream.JsonReader jsonReader = reader.getJsonReader();
+ try {
+ jsonReader.beginObject();
+ jsonReader.nextName();
+ jsonReader.nextString();
+ fail("An over-sized body was read in full");
+ } catch (Exception e) {
+ assertExceedsCeiling(e);
+ }
+ }
+
+ private void assertMoshiReaderBounded(Builder builder) throws Exception {
+ builder.processDocument(body(1000), "application/json",
messageContext);
+ MoshiXMLStreamReader reader = (MoshiXMLStreamReader)
+
messageContext.getProperty(JsonConstant.MOSHI_XML_STREAM_READER);
+ com.squareup.moshi.JsonReader jsonReader = reader.getJsonReader();
+ try {
+ jsonReader.beginObject();
+ jsonReader.nextName();
+ jsonReader.nextString();
+ fail("An over-sized body was read in full");
+ } catch (Exception e) {
+ assertExceedsCeiling(e);
+ }
+ }
+}
diff --git a/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
b/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
index ce9461b37d..8e69478383 100644
--- a/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
+++ b/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
@@ -61,7 +61,6 @@ public final class RequestSizeLimits {
/** 2 MB: form encoding is for field data, not bulk transfer. */
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>
@@ -82,6 +81,23 @@ public final class RequestSizeLimits {
/** Default {@link #SOAP_MAX_REQUEST_SIZE}: 100 MB. */
public static final long DEFAULT_SOAP_MAX_REQUEST_SIZE = 100L * 1024 *
1024;
+ /**
+ * Ceiling on a JSON body, for every builder in the axis2-json module.
+ * <p>
+ * JSON is the primary protocol, so this is the ceiling most deployments
+ * actually rely on. Streaming does not make a JSON builder safe to leave
+ * unbounded: a single string token is still materialised whole.
+ */
+ public static final String JSON_MAX_REQUEST_SIZE = "jsonMaxRequestSize";
+
+ /**
+ * Default {@link #JSON_MAX_REQUEST_SIZE}: 100 MB, matching SOAP. The
HTTP/2
+ * JSON builders are tuned for payloads of 50 MB and more, so a smaller
+ * default would break the deployments they were written for.
+ */
+ public static final long DEFAULT_JSON_MAX_REQUEST_SIZE = 100L * 1024 *
1024;
+
+ /** Sentinel for "no ceiling", matching the commons-fileupload2
convention. */
public static final long UNLIMITED = -1L;
private RequestSizeLimits() {
diff --git a/src/site/markdown/release-notes/2.0.2.md
b/src/site/markdown/release-notes/2.0.2.md
index 3b789d4d61..e1628e8480 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -198,8 +198,9 @@ in `SECURITY.md`.
so a servlet container's post-size limit never saw the body.
`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
+ whole `multipart/related` body and so MTOM and SwA), `soapMaxRequestSize`
+ (100 MB, a plain SOAP or POX body) and `jsonMaxRequestSize` (100 MB, every
JSON
+ builder including the Gson, Moshi and HTTP/2 variants) 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