This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 8ac63e535 WW-5618 feat(json): add configurable limits to JSON plugin
(#1625)
8ac63e535 is described below
commit 8ac63e535aab02c0912e7fcc41c2f5aa33e4e1bd
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Mar 21 12:11:06 2026 +0100
WW-5618 feat(json): add configurable limits to JSON plugin (#1625)
Add configurable limits to the JSON plugin to prevent denial-of-service
attacks via malicious payloads (deeply nested objects, huge arrays, long
strings).
Changes:
- Extract JSONReader interface from class, create StrutsJSONReader impl
with maxElements, maxDepth, maxStringLength, maxKeyLength enforcement
- Rename DefaultJSONWriter to StrutsJSONWriter (Struts* naming convention)
- Add JSONBeanSelectionProvider for bean aliasing via constants
- Update JSONUtil with @Inject for reader/writer, add instance
deserializeInput() with maxLength check, deprecate static deserialize()
- Wire limits into JSONInterceptor with @Inject from constants
- Register beans and defaults in struts-plugin.xml
Default limits: 10K elements, 64 depth, 2MB length, 256KB strings, 512 keys.
All configurable via struts.xml constants or per-action interceptor params.
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
...nstants.java => JSONBeanSelectionProvider.java} | 25 +-
.../org/apache/struts2/json/JSONConstants.java | 6 +
.../org/apache/struts2/json/JSONInterceptor.java | 53 ++-
.../java/org/apache/struts2/json/JSONReader.java | 282 +------------
.../java/org/apache/struts2/json/JSONUtil.java | 50 ++-
.../{JSONReader.java => StrutsJSONReader.java} | 210 ++++++----
...efaultJSONWriter.java => StrutsJSONWriter.java} | 4 +-
.../json/config/entities/JSONConstantConfig.java | 64 +++
plugins/json/src/main/resources/struts-plugin.xml | 16 +-
.../java/org/apache/struts2/json/JSONEnumTest.java | 8 +-
.../apache/struts2/json/JSONInterceptorTest.java | 94 ++++-
.../org/apache/struts2/json/JSONPopulatorTest.java | 17 +-
.../org/apache/struts2/json/JSONReaderTest.java | 2 +-
.../org/apache/struts2/json/JSONResultTest.java | 48 +--
.../java/org/apache/struts2/json/JSONUtilTest.java | 6 +-
.../apache/struts2/json/StrutsJSONReaderTest.java | 159 ++++++++
...ONWriterTest.java => StrutsJSONWriterTest.java} | 47 +--
.../struts2/json/jsonwriter-write-bean-02.txt | 2 +-
...6-03-16-json-plugin-configurable-limits-plan.md | 450 +++++++++++++++++++++
19 files changed, 1077 insertions(+), 466 deletions(-)
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java
b/plugins/json/src/main/java/org/apache/struts2/json/JSONBeanSelectionProvider.java
similarity index 55%
copy from plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java
copy to
plugins/json/src/main/java/org/apache/struts2/json/JSONBeanSelectionProvider.java
index f5cb292bd..f13e5af75 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java
+++
b/plugins/json/src/main/java/org/apache/struts2/json/JSONBeanSelectionProvider.java
@@ -18,17 +18,18 @@
*/
package org.apache.struts2.json;
-/**
- * <p>Class consisting of various constant values being used controlling
- * JSON plugin behaviour</p>
- *
- * <p>
- * These values can be overridden using struts.xml file by providing custom
values.
- * </p>
- */
-public class JSONConstants {
+import org.apache.struts2.config.AbstractBeanSelectionProvider;
+import org.apache.struts2.config.ConfigurationException;
+import org.apache.struts2.inject.ContainerBuilder;
+import org.apache.struts2.inject.Scope;
+import org.apache.struts2.util.location.LocatableProperties;
+
+public class JSONBeanSelectionProvider extends AbstractBeanSelectionProvider {
+
+ @Override
+ public void register(ContainerBuilder builder, LocatableProperties props)
throws ConfigurationException {
+ alias(JSONReader.class, JSONConstants.JSON_READER, builder, props,
Scope.PROTOTYPE);
+ alias(JSONWriter.class, JSONConstants.JSON_WRITER, builder, props,
Scope.PROTOTYPE);
+ }
- public static final String JSON_WRITER = "struts.json.writer";
- public static final String RESULT_EXCLUDE_PROXY_PROPERTIES =
"struts.json.result.excludeProxyProperties";
- public static final String DATE_FORMAT = "struts.json.dateformat";
}
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java
b/plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java
index f5cb292bd..5e8bb5fdc 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java
@@ -29,6 +29,12 @@ package org.apache.struts2.json;
public class JSONConstants {
public static final String JSON_WRITER = "struts.json.writer";
+ public static final String JSON_READER = "struts.json.reader";
public static final String RESULT_EXCLUDE_PROXY_PROPERTIES =
"struts.json.result.excludeProxyProperties";
public static final String DATE_FORMAT = "struts.json.dateformat";
+ public static final String JSON_MAX_ELEMENTS = "struts.json.maxElements";
+ public static final String JSON_MAX_DEPTH = "struts.json.maxDepth";
+ public static final String JSON_MAX_LENGTH = "struts.json.maxLength";
+ public static final String JSON_MAX_STRING_LENGTH =
"struts.json.maxStringLength";
+ public static final String JSON_MAX_KEY_LENGTH =
"struts.json.maxKeyLength";
}
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
index df8609ab6..6511da033 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java
@@ -71,6 +71,13 @@ public class JSONInterceptor extends AbstractInterceptor {
private String jsonContentType = "application/json";
private String jsonRpcContentType = "application/json-rpc";
+ private JSONUtil jsonUtil;
+ private int maxElements = JSONReader.DEFAULT_MAX_ELEMENTS;
+ private int maxDepth = JSONReader.DEFAULT_MAX_DEPTH;
+ private int maxLength = 2_097_152; // 2MB
+ private int maxStringLength = JSONReader.DEFAULT_MAX_STRING_LENGTH;
+ private int maxKeyLength = JSONReader.DEFAULT_MAX_KEY_LENGTH;
+
@SuppressWarnings("unchecked")
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
@@ -91,7 +98,8 @@ public class JSONInterceptor extends AbstractInterceptor {
if (jsonContentType.equalsIgnoreCase(requestContentType)) {
// load JSON object
- Object obj = JSONUtil.deserialize(request.getReader());
+ applyLimitsToReader();
+ Object obj = jsonUtil.deserializeInput(request.getReader(),
maxLength);
// JSON array (this.root cannot be null in this case)
if(obj instanceof List && this.root != null) {
@@ -133,7 +141,8 @@ public class JSONInterceptor extends AbstractInterceptor {
Object result;
if (this.enableSMD) {
// load JSON object
- Object obj = JSONUtil.deserialize(request.getReader());
+ applyLimitsToReader();
+ Object obj = jsonUtil.deserializeInput(request.getReader(),
maxLength);
if (obj instanceof Map) {
Map smd = (Map) obj;
@@ -168,8 +177,6 @@ public class JSONInterceptor extends AbstractInterceptor {
result = rpcResponse;
}
- JSONUtil jsonUtil =
invocation.getInvocationContext().getContainer().getInstance(JSONUtil.class);
-
String json = jsonUtil.serialize(result, excludeProperties,
getIncludeProperties(),
ignoreHierarchy, excludeNullProperties);
json = addCallbackIfApplicable(request, json);
@@ -185,6 +192,14 @@ public class JSONInterceptor extends AbstractInterceptor {
return invocation.invoke();
}
+ private void applyLimitsToReader() {
+ JSONReader reader = jsonUtil.getReader();
+ reader.setMaxElements(maxElements);
+ reader.setMaxDepth(maxDepth);
+ reader.setMaxStringLength(maxStringLength);
+ reader.setMaxKeyLength(maxKeyLength);
+ }
+
protected String readContentType(HttpServletRequest request) {
String contentType = request.getHeader("Content-Type");
LOG.debug("Content Type from request: {}", contentType);
@@ -564,4 +579,34 @@ public class JSONInterceptor extends AbstractInterceptor {
public void setJsonRpcContentType(String jsonRpcContentType) {
this.jsonRpcContentType = jsonRpcContentType;
}
+
+ @Inject
+ public void setJsonUtil(JSONUtil jsonUtil) {
+ this.jsonUtil = jsonUtil;
+ }
+
+ @Inject(value = JSONConstants.JSON_MAX_ELEMENTS, required = false)
+ public void setMaxElements(String maxElements) {
+ this.maxElements = Integer.parseInt(maxElements);
+ }
+
+ @Inject(value = JSONConstants.JSON_MAX_DEPTH, required = false)
+ public void setMaxDepth(String maxDepth) {
+ this.maxDepth = Integer.parseInt(maxDepth);
+ }
+
+ @Inject(value = JSONConstants.JSON_MAX_LENGTH, required = false)
+ public void setMaxLength(String maxLength) {
+ this.maxLength = Integer.parseInt(maxLength);
+ }
+
+ @Inject(value = JSONConstants.JSON_MAX_STRING_LENGTH, required = false)
+ public void setMaxStringLength(String maxStringLength) {
+ this.maxStringLength = Integer.parseInt(maxStringLength);
+ }
+
+ @Inject(value = JSONConstants.JSON_MAX_KEY_LENGTH, required = false)
+ public void setMaxKeyLength(String maxKeyLength) {
+ this.maxKeyLength = Integer.parseInt(maxKeyLength);
+ }
}
diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
b/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
index 4fb1c406e..1af39ac59 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
@@ -18,285 +18,25 @@
*/
package org.apache.struts2.json;
-import java.text.CharacterIterator;
-import java.text.StringCharacterIterator;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
/**
* <p>
- * Deserializes and object from a JSON string
+ * Deserializes an object from a JSON string.
* </p>
*/
-public class JSONReader {
- private static final Object OBJECT_END = new Object();
- private static final Object ARRAY_END = new Object();
- private static final Object COLON = new Object();
- private static final Object COMMA = new Object();
- private static Map<Character, Character> escapes = new HashMap<Character,
Character>();
-
- static {
- escapes.put('"', '"');
- escapes.put('\\', '\\');
- escapes.put('/', '/');
- escapes.put('b', '\b');
- escapes.put('f', '\f');
- escapes.put('n', '\n');
- escapes.put('r', '\r');
- escapes.put('t', '\t');
- }
-
- private CharacterIterator it;
- private char c;
- private Object token;
- private StringBuilder buf = new StringBuilder();
-
- protected char next() {
- this.c = this.it.next();
-
- return this.c;
- }
-
- protected void skipWhiteSpace() {
- while (Character.isWhitespace(this.c)) {
- this.next();
- }
- }
-
- public Object read(String string) throws JSONException {
- this.it = new StringCharacterIterator(string);
- this.c = this.it.first();
-
- return this.read();
- }
-
- protected Object read() throws JSONException {
- Object ret;
-
- this.skipWhiteSpace();
-
- if (this.c == '"') {
- this.next();
- ret = this.string('"');
- } else if (this.c == '\'') {
- this.next();
- ret = this.string('\'');
- } else if (this.c == '[') {
- this.next();
- ret = this.array();
- } else if (this.c == ']') {
- ret = ARRAY_END;
- this.next();
- } else if (this.c == ',') {
- ret = COMMA;
- this.next();
- } else if (this.c == '{') {
- this.next();
- ret = this.object();
- } else if (this.c == '}') {
- ret = OBJECT_END;
- this.next();
- } else if (this.c == ':') {
- ret = COLON;
- this.next();
- } else if ((this.c == 't') && (this.next() == 'r') && (this.next() ==
'u') && (this.next() == 'e')) {
- ret = Boolean.TRUE;
- this.next();
- } else if ((this.c == 'f') && (this.next() == 'a') && (this.next() ==
'l') && (this.next() == 's')
- && (this.next() == 'e')) {
- ret = Boolean.FALSE;
- this.next();
- } else if ((this.c == 'n') && (this.next() == 'u') && (this.next() ==
'l') && (this.next() == 'l')) {
- ret = null;
- this.next();
- } else if (Character.isDigit(this.c) || (this.c == '-')) {
- ret = this.number();
- } else {
- throw buildInvalidInputException();
- }
-
- this.token = ret;
-
- return ret;
- }
-
- @SuppressWarnings("unchecked")
- protected Map object() throws JSONException {
- Map ret = new HashMap();
- Object next = this.read();
- if (next != OBJECT_END) {
- String key = (String) next;
- while (this.token != OBJECT_END) {
- this.read(); // should be a colon
-
- if (this.token != OBJECT_END) {
- ret.put(key, this.read());
-
- if (this.read() == COMMA) {
- Object name = this.read();
-
- if (name instanceof String) {
- key = (String) name;
- } else
- throw buildInvalidInputException();
- }
- }
- }
- }
-
- return ret;
- }
-
- protected JSONException buildInvalidInputException() {
- return new JSONException("Input string is not well formed JSON
(invalid char " + this.c + ")");
- }
-
-
- @SuppressWarnings("unchecked")
- protected List array() throws JSONException {
- List ret = new ArrayList();
- Object value = this.read();
-
- while (this.token != ARRAY_END) {
- ret.add(value);
-
- Object read = this.read();
- if (read == COMMA) {
- value = this.read();
- } else if (read != ARRAY_END) {
- throw buildInvalidInputException();
- }
- }
-
- return ret;
- }
-
- protected Object number() throws JSONException {
- this.buf.setLength(0);
- boolean toDouble = false;
-
- if (this.c == '-') {
- this.add();
- }
-
- this.addDigits();
-
- if (this.c == '.') {
- toDouble = true;
- this.add();
- this.addDigits();
- }
-
- if ((this.c == 'e') || (this.c == 'E')) {
- toDouble = true;
- this.add();
-
- if ((this.c == '+') || (this.c == '-')) {
- this.add();
- }
-
- this.addDigits();
- }
-
- if (toDouble) {
- try {
- return Double.parseDouble(this.buf.toString());
- } catch (NumberFormatException e) {
- throw buildInvalidInputException();
- }
- } else {
- try {
- return Long.parseLong(this.buf.toString());
- } catch (NumberFormatException e) {
- throw buildInvalidInputException();
- }
- }
- }
-
- protected Object string(char quote) {
- this.buf.setLength(0);
-
- while ((this.c != quote) && (this.c != CharacterIterator.DONE)) {
- if (this.c == '\\') {
- this.next();
-
- if (this.c == 'u') {
- this.add(this.unicode());
- } else {
- Object value = escapes.get(this.c);
-
- if (value != null) {
- this.add((Character) value);
- }
- }
- } else {
- this.add();
- }
- }
-
- this.next();
-
- return this.buf.toString();
- }
-
- protected void add(char cc) {
- this.buf.append(cc);
- this.next();
- }
-
- protected void add() {
- this.add(this.c);
- }
-
- protected void addDigits() {
- while (Character.isDigit(this.c)) {
- this.add();
- }
- }
-
- protected char unicode() {
- int value = 0;
-
- for (int i = 0; i < 4; ++i) {
- switch (this.next()) {
- case '0':
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- case '7':
- case '8':
- case '9':
- value = (value << 4) + (this.c - '0');
+public interface JSONReader {
- break;
+ int DEFAULT_MAX_ELEMENTS = 10_000;
+ int DEFAULT_MAX_DEPTH = 64;
+ int DEFAULT_MAX_STRING_LENGTH = 262_144; // 256KB
+ int DEFAULT_MAX_KEY_LENGTH = 512;
- case 'a':
- case 'b':
- case 'c':
- case 'd':
- case 'e':
- case 'f':
- value = (value << 4) + (this.c - 'W');
+ Object read(String string) throws JSONException;
- break;
+ void setMaxElements(int maxElements);
- case 'A':
- case 'B':
- case 'C':
- case 'D':
- case 'E':
- case 'F':
- value = (value << 4) + (this.c - '7');
+ void setMaxDepth(int maxDepth);
- break;
- }
- }
+ void setMaxStringLength(int maxStringLength);
- return (char) value;
- }
+ void setMaxKeyLength(int maxKeyLength);
}
diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java
b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java
index ab23f4dcd..78bceaacf 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java
@@ -40,7 +40,6 @@ import java.util.Arrays;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
-import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
@@ -60,16 +59,21 @@ public class JSONUtil {
private static final Logger LOG = LogManager.getLogger(JSONUtil.class);
+ private JSONReader reader;
private JSONWriter writer;
- public void setWriter(JSONWriter writer) {
- this.writer = writer;
+ @Inject
+ public void setReader(JSONReader reader) {
+ this.reader = reader;
+ }
+
+ public JSONReader getReader() {
+ return reader;
}
@Inject
- public void setContainer(Container container) {
- setWriter(container.getInstance(JSONWriter.class,
container.getInstance(String.class,
- JSONConstants.JSON_WRITER)));
+ public void setWriter(JSONWriter writer) {
+ this.writer = writer;
}
/**
@@ -284,6 +288,34 @@ public class JSONUtil {
writer.write(serialize(object, excludeProperties, includeProperties,
true, excludeNullProperties, cacheBeanInfo));
}
+ /**
+ * Deserializes an object from JSON using the injected reader with limit
enforcement.
+ *
+ * @param reader Reader to read a JSON string from
+ * @param maxLength maximum allowed length of the JSON input
+ * @return deserialized object
+ * @throws JSONException when IOException happens or limits are exceeded
+ */
+ public Object deserializeInput(Reader reader, int maxLength) throws
JSONException {
+ BufferedReader bufferReader = new BufferedReader(reader);
+ String line;
+ StringBuilder buffer = new StringBuilder();
+
+ try {
+ while ((line = bufferReader.readLine()) != null) {
+ buffer.append(line);
+ if (buffer.length() > maxLength) {
+ throw new JSONException("JSON input exceeds maximum
allowed length ("
+ + maxLength + "). Use " +
JSONConstants.JSON_MAX_LENGTH + " to increase the limit.");
+ }
+ }
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+
+ return this.reader.read(buffer.toString());
+ }
+
/**
* Deserializes a object from JSON
*
@@ -291,9 +323,11 @@ public class JSONUtil {
* string in JSON
* @return desrialized object
* @throws JSONException in case of error during serialize
+ * @deprecated Use instance method {@link #deserializeInput(Reader, int)}
with injected JSONUtil instead
*/
+ @Deprecated( forRemoval = true, since = "7.2.0")
public static Object deserialize(String json) throws JSONException {
- JSONReader reader = new JSONReader();
+ StrutsJSONReader reader = new StrutsJSONReader();
return reader.read(json);
}
@@ -305,7 +339,9 @@ public class JSONUtil {
* @return deserialized object
* @throws JSONException
* when IOException happens
+ * @deprecated Use instance method {@link #deserializeInput(Reader, int)}
with injected JSONUtil instead
*/
+ @Deprecated( forRemoval = true, since = "7.2.0")
public static Object deserialize(Reader reader) throws JSONException {
// read content
BufferedReader bufferReader = new BufferedReader(reader);
diff --git a/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java
similarity index 52%
copy from plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
copy to plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java
index 4fb1c406e..530a479cc 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java
@@ -27,31 +27,56 @@ import java.util.Map;
/**
* <p>
- * Deserializes and object from a JSON string
+ * Deserializes an object from a JSON string with configurable limits
+ * to prevent denial-of-service attacks via malicious payloads.
* </p>
*/
-public class JSONReader {
+public class StrutsJSONReader implements JSONReader {
private static final Object OBJECT_END = new Object();
private static final Object ARRAY_END = new Object();
private static final Object COLON = new Object();
private static final Object COMMA = new Object();
- private static Map<Character, Character> escapes = new HashMap<Character,
Character>();
-
- static {
- escapes.put('"', '"');
- escapes.put('\\', '\\');
- escapes.put('/', '/');
- escapes.put('b', '\b');
- escapes.put('f', '\f');
- escapes.put('n', '\n');
- escapes.put('r', '\r');
- escapes.put('t', '\t');
- }
+ private static final Map<Character, Character> escapes = Map.of(
+ '"', '"',
+ '\\', '\\',
+ '/', '/',
+ 'b', '\b',
+ 'f', '\f',
+ 'n', '\n',
+ 'r', '\r',
+ 't', '\t'
+ );
private CharacterIterator it;
private char c;
private Object token;
- private StringBuilder buf = new StringBuilder();
+ private final StringBuilder buf = new StringBuilder();
+
+ private int maxElements = DEFAULT_MAX_ELEMENTS;
+ private int maxDepth = DEFAULT_MAX_DEPTH;
+ private int maxStringLength = DEFAULT_MAX_STRING_LENGTH;
+ private int maxKeyLength = DEFAULT_MAX_KEY_LENGTH;
+ private int depth;
+
+ @Override
+ public void setMaxElements(int maxElements) {
+ this.maxElements = maxElements;
+ }
+
+ @Override
+ public void setMaxDepth(int maxDepth) {
+ this.maxDepth = maxDepth;
+ }
+
+ @Override
+ public void setMaxStringLength(int maxStringLength) {
+ this.maxStringLength = maxStringLength;
+ }
+
+ @Override
+ public void setMaxKeyLength(int maxKeyLength) {
+ this.maxKeyLength = maxKeyLength;
+ }
protected char next() {
this.c = this.it.next();
@@ -65,9 +90,11 @@ public class JSONReader {
}
}
+ @Override
public Object read(String string) throws JSONException {
this.it = new StringCharacterIterator(string);
this.c = this.it.first();
+ this.depth = 0;
return this.read();
}
@@ -122,55 +149,89 @@ public class JSONReader {
return ret;
}
- @SuppressWarnings("unchecked")
- protected Map object() throws JSONException {
- Map ret = new HashMap();
- Object next = this.read();
- if (next != OBJECT_END) {
- String key = (String) next;
- while (this.token != OBJECT_END) {
- this.read(); // should be a colon
-
- if (this.token != OBJECT_END) {
- ret.put(key, this.read());
-
- if (this.read() == COMMA) {
- Object name = this.read();
-
- if (name instanceof String) {
- key = (String) name;
- } else
- throw buildInvalidInputException();
+ protected Map<String, Object> object() throws JSONException {
+ if (this.depth >= this.maxDepth) {
+ throw new JSONException("JSON object nesting exceeds maximum
allowed depth ("
+ + this.maxDepth + "). Use " + JSONConstants.JSON_MAX_DEPTH
+ " to increase the limit.");
+ }
+ this.depth++;
+ try {
+ Map<String, Object> ret = new HashMap<>();
+ Object next = this.read();
+ if (next != OBJECT_END) {
+ String key = (String) next;
+ validateKeyLength(key);
+ while (this.token != OBJECT_END) {
+ this.read(); // should be a colon
+
+ if (this.token != OBJECT_END) {
+ if (ret.size() >= this.maxElements) {
+ throw new JSONException("JSON object exceeds
maximum allowed elements ("
+ + this.maxElements + "). Use " +
JSONConstants.JSON_MAX_ELEMENTS + " to increase the limit.");
+ }
+ ret.put(key, this.read());
+
+ if (this.read() == COMMA) {
+ Object name = this.read();
+
+ if (name instanceof String nextKey) {
+ key = nextKey;
+ validateKeyLength(key);
+ } else {
+ throw buildInvalidInputException();
+ }
+ }
}
}
}
+
+ return ret;
+ } finally {
+ this.depth--;
}
+ }
- return ret;
+ private void validateKeyLength(String key) throws JSONException {
+ if (key.length() > this.maxKeyLength) {
+ throw new JSONException("JSON object key exceeds maximum allowed
length ("
+ + this.maxKeyLength + "). Use " +
JSONConstants.JSON_MAX_KEY_LENGTH + " to increase the limit.");
+ }
}
protected JSONException buildInvalidInputException() {
return new JSONException("Input string is not well formed JSON
(invalid char " + this.c + ")");
}
-
- @SuppressWarnings("unchecked")
- protected List array() throws JSONException {
- List ret = new ArrayList();
- Object value = this.read();
- while (this.token != ARRAY_END) {
- ret.add(value);
+ protected List<Object> array() throws JSONException {
+ if (this.depth >= this.maxDepth) {
+ throw new JSONException("JSON array nesting exceeds maximum
allowed depth ("
+ + this.maxDepth + "). Use " + JSONConstants.JSON_MAX_DEPTH
+ " to increase the limit.");
+ }
+ this.depth++;
+ try {
+ List<Object> ret = new ArrayList<>();
+ Object value = this.read();
+
+ while (this.token != ARRAY_END) {
+ if (ret.size() >= this.maxElements) {
+ throw new JSONException("JSON array exceeds maximum
allowed elements ("
+ + this.maxElements + "). Use " +
JSONConstants.JSON_MAX_ELEMENTS + " to increase the limit.");
+ }
+ ret.add(value);
- Object read = this.read();
- if (read == COMMA) {
- value = this.read();
- } else if (read != ARRAY_END) {
- throw buildInvalidInputException();
+ Object read = this.read();
+ if (read == COMMA) {
+ value = this.read();
+ } else if (read != ARRAY_END) {
+ throw buildInvalidInputException();
+ }
}
- }
- return ret;
+ return ret;
+ } finally {
+ this.depth--;
+ }
}
protected Object number() throws JSONException {
@@ -215,7 +276,7 @@ public class JSONReader {
}
}
- protected Object string(char quote) {
+ protected Object string(char quote) throws JSONException {
this.buf.setLength(0);
while ((this.c != quote) && (this.c != CharacterIterator.DONE)) {
@@ -225,15 +286,19 @@ public class JSONReader {
if (this.c == 'u') {
this.add(this.unicode());
} else {
- Object value = escapes.get(this.c);
+ Character value = escapes.get(this.c);
if (value != null) {
- this.add((Character) value);
+ this.add(value);
}
}
} else {
this.add();
}
+ if (this.buf.length() > this.maxStringLength) {
+ throw new JSONException("JSON string exceeds maximum allowed
length ("
+ + this.maxStringLength + "). Use " +
JSONConstants.JSON_MAX_STRING_LENGTH + " to increase the limit.");
+ }
}
this.next();
@@ -260,41 +325,12 @@ public class JSONReader {
int value = 0;
for (int i = 0; i < 4; ++i) {
- switch (this.next()) {
- case '0':
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- case '7':
- case '8':
- case '9':
- value = (value << 4) + (this.c - '0');
-
- break;
-
- case 'a':
- case 'b':
- case 'c':
- case 'd':
- case 'e':
- case 'f':
- value = (value << 4) + (this.c - 'W');
-
- break;
-
- case 'A':
- case 'B':
- case 'C':
- case 'D':
- case 'E':
- case 'F':
- value = (value << 4) + (this.c - '7');
-
- break;
- }
+ value = switch (this.next()) {
+ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ->
(value << 4) + (this.c - '0');
+ case 'a', 'b', 'c', 'd', 'e', 'f' -> (value << 4) + (this.c -
'W');
+ case 'A', 'B', 'C', 'D', 'E', 'F' -> (value << 4) + (this.c -
'7');
+ default -> value;
+ };
}
return (char) value;
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java
b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java
similarity index 99%
rename from
plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java
rename to
plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java
index f911139fc..c75e54e0a 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java
@@ -67,9 +67,9 @@ import java.util.regex.Pattern;
* references are detected they will be nulled out.
* </p>
*/
-public class DefaultJSONWriter implements JSONWriter {
+public class StrutsJSONWriter implements JSONWriter {
- private static final Logger LOG =
LogManager.getLogger(DefaultJSONWriter.class);
+ private static final Logger LOG =
LogManager.getLogger(StrutsJSONWriter.class);
private static final char[] hex = "0123456789ABCDEF".toCharArray();
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/config/entities/JSONConstantConfig.java
b/plugins/json/src/main/java/org/apache/struts2/json/config/entities/JSONConstantConfig.java
index ee7fab60f..580ee7e7a 100644
---
a/plugins/json/src/main/java/org/apache/struts2/json/config/entities/JSONConstantConfig.java
+++
b/plugins/json/src/main/java/org/apache/struts2/json/config/entities/JSONConstantConfig.java
@@ -27,16 +27,28 @@ import org.apache.struts2.json.JSONConstants;
public class JSONConstantConfig extends ConstantConfig {
private BeanConfig jsonWriter;
+ private BeanConfig jsonReader;
private Boolean jsonResultExcludeProxyProperties;
private String jsonDateFormat;
+ private Integer jsonMaxElements;
+ private Integer jsonMaxDepth;
+ private Integer jsonMaxLength;
+ private Integer jsonMaxStringLength;
+ private Integer jsonMaxKeyLength;
@Override
public Map<String, String> getAllAsStringsMap() {
Map<String, String> map = super.getAllAsStringsMap();
map.put(JSONConstants.JSON_WRITER, beanConfToString(jsonWriter));
+ map.put(JSONConstants.JSON_READER, beanConfToString(jsonReader));
map.put(JSONConstants.RESULT_EXCLUDE_PROXY_PROPERTIES,
Objects.toString(jsonResultExcludeProxyProperties, null));
map.put(JSONConstants.DATE_FORMAT, jsonDateFormat);
+ map.put(JSONConstants.JSON_MAX_ELEMENTS,
Objects.toString(jsonMaxElements, null));
+ map.put(JSONConstants.JSON_MAX_DEPTH, Objects.toString(jsonMaxDepth,
null));
+ map.put(JSONConstants.JSON_MAX_LENGTH, Objects.toString(jsonMaxLength,
null));
+ map.put(JSONConstants.JSON_MAX_STRING_LENGTH,
Objects.toString(jsonMaxStringLength, null));
+ map.put(JSONConstants.JSON_MAX_KEY_LENGTH,
Objects.toString(jsonMaxKeyLength, null));
return map;
}
@@ -68,4 +80,56 @@ public class JSONConstantConfig extends ConstantConfig {
public void setJsonDateFormat(String jsonDateFormat) {
this.jsonDateFormat = jsonDateFormat;
}
+
+ public BeanConfig getJsonReader() {
+ return jsonReader;
+ }
+
+ public void setJsonReader(BeanConfig jsonReader) {
+ this.jsonReader = jsonReader;
+ }
+
+ public void setJsonReader(Class<?> clazz) {
+ this.jsonReader = new BeanConfig(clazz, clazz.getName());
+ }
+
+ public Integer getJsonMaxElements() {
+ return jsonMaxElements;
+ }
+
+ public void setJsonMaxElements(Integer jsonMaxElements) {
+ this.jsonMaxElements = jsonMaxElements;
+ }
+
+ public Integer getJsonMaxDepth() {
+ return jsonMaxDepth;
+ }
+
+ public void setJsonMaxDepth(Integer jsonMaxDepth) {
+ this.jsonMaxDepth = jsonMaxDepth;
+ }
+
+ public Integer getJsonMaxLength() {
+ return jsonMaxLength;
+ }
+
+ public void setJsonMaxLength(Integer jsonMaxLength) {
+ this.jsonMaxLength = jsonMaxLength;
+ }
+
+ public Integer getJsonMaxStringLength() {
+ return jsonMaxStringLength;
+ }
+
+ public void setJsonMaxStringLength(Integer jsonMaxStringLength) {
+ this.jsonMaxStringLength = jsonMaxStringLength;
+ }
+
+ public Integer getJsonMaxKeyLength() {
+ return jsonMaxKeyLength;
+ }
+
+ public void setJsonMaxKeyLength(Integer jsonMaxKeyLength) {
+ this.jsonMaxKeyLength = jsonMaxKeyLength;
+ }
}
diff --git a/plugins/json/src/main/resources/struts-plugin.xml
b/plugins/json/src/main/resources/struts-plugin.xml
index 1291246a7..88451b39b 100644
--- a/plugins/json/src/main/resources/struts-plugin.xml
+++ b/plugins/json/src/main/resources/struts-plugin.xml
@@ -24,12 +24,20 @@
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
- <bean type="org.apache.struts2.json.JSONWriter" name="struts"
class="org.apache.struts2.json.DefaultJSONWriter"
+ <bean type="org.apache.struts2.json.JSONWriter" name="struts"
class="org.apache.struts2.json.StrutsJSONWriter"
+ scope="prototype"/>
+ <bean type="org.apache.struts2.json.JSONReader" name="struts"
class="org.apache.struts2.json.StrutsJSONReader"
scope="prototype"/>
- <constant name="struts.json.writer" value="struts"/>
- <!-- TODO: Make DefaultJSONWriter thread-safe to remove "prototype"s -->
<bean class="org.apache.struts2.json.JSONUtil" scope="prototype"/>
+ <constant name="struts.json.writer" value="struts"/>
+ <constant name="struts.json.reader" value="struts"/>
+ <constant name="struts.json.maxElements" value="10000"/>
+ <constant name="struts.json.maxDepth" value="64"/>
+ <constant name="struts.json.maxLength" value="2097152"/>
+ <constant name="struts.json.maxStringLength" value="262144"/>
+ <constant name="struts.json.maxKeyLength" value="512"/>
+
<package name="json-default" extends="struts-default">
<result-types>
@@ -54,4 +62,6 @@
</interceptors>
</package>
+
+ <bean-selection name="jsonBeans"
class="org.apache.struts2.json.JSONBeanSelectionProvider"/>
</struts>
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java
index a9ee11331..648e8f7b4 100644
--- a/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java
+++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONEnumTest.java
@@ -44,11 +44,11 @@ public class JSONEnumTest extends TestCase {
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
String json = jsonWriter.write(bean1);
- Map result = (Map) JSONUtil.deserialize(json);
+ Map result = (Map) new StrutsJSONReader().read(json);
assertEquals("str", result.get("stringField"));
assertEquals(true, result.get("booleanField"));
assertEquals("s", result.get("charField")); // note: this is a
@@ -86,11 +86,11 @@ public class JSONEnumTest extends TestCase {
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(true);
String json = jsonWriter.write(bean1);
- Map result = (Map) JSONUtil.deserialize(json);
+ Map result = (Map) new StrutsJSONReader().read(json);
assertEquals("str", result.get("stringField"));
assertEquals(true, result.get("booleanField"));
assertEquals("s", result.get("charField")); // note: this is a
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
index a5cfd2e1c..9f5c4a75f 100644
---
a/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
+++
b/plugins/json/src/test/java/org/apache/struts2/json/JSONInterceptorTest.java
@@ -41,6 +41,15 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.setContent(content.getBytes());
}
+ private JSONInterceptor createInterceptor() {
+ JSONInterceptor interceptor = new JSONInterceptor();
+ JSONUtil jsonUtil = new JSONUtil();
+ jsonUtil.setReader(new StrutsJSONReader());
+ jsonUtil.setWriter(new StrutsJSONWriter());
+ interceptor.setJsonUtil(jsonUtil);
+ return interceptor;
+ }
+
public void testBadJSON1() throws Exception {
tryBadJSON("bad-1.txt");
}
@@ -70,7 +79,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent(fileName);
this.request.addHeader("Content-Type", "application/json;
charset=UTF-8");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -91,7 +100,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-3.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
SMDActionTest1 action = new SMDActionTest1();
this.invocation.setAction(action);
@@ -110,7 +119,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-14.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest2 action = new SMDActionTest2();
@@ -128,7 +137,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-15.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest2 action = new SMDActionTest2();
@@ -146,7 +155,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-4.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -170,7 +179,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-9.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -191,7 +200,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-6.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -226,7 +235,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-10.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest2 action = new SMDActionTest2();
@@ -251,7 +260,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
setRequestContent("smd-7.txt");
this.request.addHeader("Content-Type", "application/json-rpc");
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setEnableSMD(true);
SMDActionTest1 action = new SMDActionTest1();
@@ -300,7 +309,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
TestAction action = new TestAction();
this.invocation.setAction(action);
@@ -315,7 +324,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
TestAction action = new TestAction();
this.invocation.setAction(action);
@@ -437,7 +446,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setRoot("bean");
TestAction4 action = new TestAction4();
@@ -462,7 +471,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setRoot("beans");
TestAction5 action = new TestAction5();
@@ -488,7 +497,7 @@ public class JSONInterceptorTest extends StrutsTestCase {
this.request.addHeader("Content-Type", "application/json");
// interceptor
- JSONInterceptor interceptor = new JSONInterceptor();
+ JSONInterceptor interceptor = createInterceptor();
interceptor.setRoot("anotherBean.yetAnotherBean.beans");
TestAction5 action = new TestAction5();
@@ -509,6 +518,63 @@ public class JSONInterceptorTest extends StrutsTestCase {
assertEquals(beans.get(0).getByteField(), 3);
}
+ public void testMaxLengthExceededThrows() throws Exception {
+ // Body is 27 bytes, set maxLength to 10
+ this.request.setContent("{\"a\":1, \"b\":2,
\"c\":\"hello\"}".getBytes());
+ this.request.addHeader("Content-Type", "application/json");
+
+ JSONInterceptor interceptor = createInterceptor();
+ interceptor.setMaxLength("10");
+ TestAction action = new TestAction();
+
+ this.invocation.setAction(action);
+
+ try {
+ interceptor.intercept(this.invocation);
+ fail("Should have thrown JSONException for exceeding maxLength");
+ } catch (JSONException e) {
+ assertTrue(e.getMessage().contains(JSONConstants.JSON_MAX_LENGTH));
+ }
+ }
+
+ public void testMaxDepthEnforcedThroughInterceptor() throws Exception {
+ // Nested 3 levels deep, set maxDepth to 2
+ this.request.setContent("{\"a\":{\"b\":{\"c\":1}}}".getBytes());
+ this.request.addHeader("Content-Type", "application/json");
+
+ JSONInterceptor interceptor = createInterceptor();
+ interceptor.setMaxDepth("2");
+ TestAction action = new TestAction();
+
+ this.invocation.setAction(action);
+
+ try {
+ interceptor.intercept(this.invocation);
+ fail("Should have thrown JSONException for exceeding maxDepth");
+ } catch (JSONException e) {
+ assertTrue(e.getMessage().contains("maximum allowed depth"));
+ }
+ }
+
+ public void testMaxElementsEnforcedThroughInterceptor() throws Exception {
+ // JSON object with 5 keys, set maxElements to 3
+ this.request.setContent("{\"a\":1, \"b\":2, \"c\":3, \"d\":4,
\"e\":5}".getBytes());
+ this.request.addHeader("Content-Type", "application/json");
+
+ JSONInterceptor interceptor = createInterceptor();
+ interceptor.setMaxElements("3");
+ TestAction action = new TestAction();
+
+ this.invocation.setAction(action);
+
+ try {
+ interceptor.intercept(this.invocation);
+ fail("Should have thrown JSONException for exceeding maxElements");
+ } catch (JSONException e) {
+ assertTrue(e.getMessage().contains("maximum allowed elements"));
+ }
+ }
+
@Override
protected void setUp() throws Exception {
super.setUp();
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java
index bf44b2ef9..e61b81ea6 100644
--- a/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java
+++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONPopulatorTest.java
@@ -22,7 +22,6 @@ import org.apache.struts2.junit.util.TestUtils;
import org.junit.Test;
import java.beans.IntrospectionException;
-import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
@@ -74,9 +73,8 @@ public class JSONPopulatorTest {
@Test
public void testPrimitiveBean() throws Exception {
- StringReader stringReader = new
StringReader(TestUtils.readContent(JSONInterceptorTest.class
- .getResource("json-7.txt")));
- Object json = JSONUtil.deserialize(stringReader);
+ String text =
TestUtils.readContent(JSONInterceptorTest.class.getResource("json-7.txt"));
+ Object json = new StrutsJSONReader().read(text);
assertNotNull(json);
assertTrue(json instanceof Map);
Map<?, ?> jsonMap = (Map<?, ?>) json;
@@ -96,7 +94,7 @@ public class JSONPopulatorTest {
@Test
public void testObjectBean() throws Exception {
String text =
TestUtils.readContent(JSONInterceptorTest.class.getResource("json-7.txt"));
- Object json = JSONUtil.deserialize(text);
+ Object json = new StrutsJSONReader().read(text);
assertNotNull(json);
assertTrue(json instanceof Map);
Map<?, ?> jsonMap = (Map<?, ?>) json;
@@ -162,9 +160,8 @@ public class JSONPopulatorTest {
@Test
public void testObjectBeanWithStrings() throws Exception {
- StringReader stringReader = new
StringReader(TestUtils.readContent(JSONInterceptorTest.class
- .getResource("json-8.txt")));
- Object json = JSONUtil.deserialize(stringReader);
+ String text =
TestUtils.readContent(JSONInterceptorTest.class.getResource("json-8.txt"));
+ Object json = new StrutsJSONReader().read(text);
assertNotNull(json);
assertTrue(json instanceof Map);
Map<?, ?> jsonMap = (Map<?, ?>) json;
@@ -187,7 +184,7 @@ public class JSONPopulatorTest {
@Test
public void testInfiniteLoop() {
try {
- JSONReader reader = new JSONReader();
+ JSONReader reader = new StrutsJSONReader();
reader.read("[1,\"a]");
fail("Should have thrown an exception");
} catch (JSONException e) {
@@ -198,7 +195,7 @@ public class JSONPopulatorTest {
@Test
public void testParseBadInput() {
try {
- JSONReader reader = new JSONReader();
+ JSONReader reader = new StrutsJSONReader();
reader.read("[1,\"a\"1]");
fail("Should have thrown an exception");
} catch (JSONException e) {
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/JSONReaderTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/JSONReaderTest.java
index 9c466c3f4..80050db6c 100644
--- a/plugins/json/src/test/java/org/apache/struts2/json/JSONReaderTest.java
+++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONReaderTest.java
@@ -29,7 +29,7 @@ import static org.junit.Assert.assertNotNull;
* Time: 17.26
*/
public class JSONReaderTest {
- private JSONReader reader = new JSONReader();
+ private StrutsJSONReader reader = new StrutsJSONReader();
@Test
public void testExponentialNumber() throws Exception {
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java
index 77831ed94..a5c938f0a 100644
--- a/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java
+++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONResultTest.java
@@ -66,7 +66,7 @@ public class JSONResultTest extends StrutsTestCase {
map.put("createtime", new Date());
try {
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
jsonUtil.serialize(map, JSONUtil.CACHE_BEAN_INFO_DEFAULT);
} catch (JSONException e) {
fail(e.getMessage());
@@ -76,14 +76,14 @@ public class JSONResultTest extends StrutsTestCase {
public void testJSONWriterEndlessLoopOnExludedProperties() throws
JSONException {
Pattern all = Pattern.compile(".*");
- JSONWriter writer = new DefaultJSONWriter();
+ JSONWriter writer = new StrutsJSONWriter();
writer.write(Arrays.asList("a", "b"), Arrays.asList(all), null, false);
}
public void testSMDDisabledSMD() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SMDActionTest1 action = new SMDActionTest1();
stack.push(action);
@@ -102,7 +102,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setEnableSMD(true);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SMDActionTest1 action = new SMDActionTest1();
stack.push(action);
@@ -122,7 +122,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setEnableSMD(true);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SMDActionTest2 action = new SMDActionTest2();
stack.push(action);
@@ -142,7 +142,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setExcludeNullProperties(true);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -161,7 +161,7 @@ public class JSONResultTest extends StrutsTestCase {
public void testNotTraverseOrIncludeProxyInfo() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
- DefaultJSONWriter writer = new DefaultJSONWriter();
+ StrutsJSONWriter writer = new StrutsJSONWriter();
writer.setProxyService(proxyService);
jsonUtil.setWriter(writer);
result.setJsonUtil(jsonUtil);
@@ -195,7 +195,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setWrapPrefix("_prefix_");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction2 action = new TestAction2();
stack.push(action);
@@ -214,7 +214,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setWrapSuffix("_suffix_");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction2 action = new TestAction2();
stack.push(action);
@@ -233,7 +233,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setDefaultDateFormat("MM-dd-yyyy");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
@@ -254,7 +254,7 @@ public class JSONResultTest extends StrutsTestCase {
result.setWrapPrefix("_prefix_");
result.setWrapSuffix("_suffix_");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction2 action = new TestAction2();
stack.push(action);
@@ -274,7 +274,7 @@ public class JSONResultTest extends StrutsTestCase {
result.setExcludeNullProperties(true);
result.setPrefix(true);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -294,7 +294,7 @@ public class JSONResultTest extends StrutsTestCase {
public void test() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
@@ -380,7 +380,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIgnoreHierarchy(false);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction3 action = new TestAction3();
@@ -399,7 +399,7 @@ public class JSONResultTest extends StrutsTestCase {
public void testCommentWrap() throws Exception {
JSONResult result = new JSONResult();
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
@@ -481,7 +481,7 @@ public class JSONResultTest extends StrutsTestCase {
private void executeTest2Action(JSONResult result) throws Exception {
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -526,7 +526,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setCallbackParameter("callback");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
request.addParameter("callback", "exec");
@@ -543,7 +543,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setNoCache(true);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -557,7 +557,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setContentType("some_super_content");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -569,7 +569,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setStatusCode(HttpServletResponse.SC_CONTINUE);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -584,7 +584,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setEnumAsBean(true);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
executeTest2Action(result);
@@ -605,7 +605,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIncludeProperties("foo");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -625,7 +625,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIncludeProperties("^list\\[\\d+\\]\\.booleanField");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
@@ -652,7 +652,7 @@ public class JSONResultTest extends StrutsTestCase {
JSONResult result = new JSONResult();
result.setIncludeProperties("^set\\[\\d+\\]\\.list\\[\\d+\\]\\.booleanField");
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
result.setJsonUtil(jsonUtil);
TestAction action = new TestAction();
stack.push(action);
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java
index d03d69e99..b2dcf8bff 100644
--- a/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java
+++ b/plugins/json/src/test/java/org/apache/struts2/json/JSONUtilTest.java
@@ -44,10 +44,10 @@ public class JSONUtilTest extends TestCase {
bean1.setEnumBean(AnEnumBean.Two);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
String json = jsonUtil.serialize(bean1,
JSONUtil.CACHE_BEAN_INFO_DEFAULT);
- Map result = (Map) JSONUtil.deserialize(json);
+ Map result = (Map) new StrutsJSONReader().read(json);
assertEquals("str", result.get("stringField"));
assertEquals(true, result.get("booleanField"));
assertEquals("s", result.get("charField")); // note: this is a
@@ -73,7 +73,7 @@ public class JSONUtilTest extends TestCase {
List<Pattern> includeProperties =
JSONUtil.processIncludePatterns(JSONUtil.asSet("listOfLists,listOfLists\\[\\d+\\]\\[\\d+\\]"),
JSONUtil.REGEXP_PATTERN);
JSONUtil jsonUtil = new JSONUtil();
- jsonUtil.setWriter(new DefaultJSONWriter());
+ jsonUtil.setWriter(new StrutsJSONWriter());
String actual = jsonUtil.serialize(bean, null, new
ArrayList<Pattern>(includeProperties), false, false);
assertEquals("{\"listOfLists\":[[\"1\",\"2\"],[\"3\",\"4\"],[\"5\",\"6\"],[\"7\",\"8\"],[\"9\",\"0\"]]}",
actual);
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONReaderTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONReaderTest.java
new file mode 100644
index 000000000..48c69c865
--- /dev/null
+++
b/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONReaderTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.struts2.json;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+public class StrutsJSONReaderTest {
+
+ @Test
+ public void testArrayExceedingMaxElementsThrows() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxElements(3);
+ String json = "[1, 2, 3, 4]";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed elements (3)"));
+ assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_ELEMENTS));
+ }
+
+ @Test
+ public void testArrayAtExactMaxElementsAllowed() throws Exception {
+ var reader = new StrutsJSONReader();
+ reader.setMaxElements(3);
+ // Exactly 3 elements: check is >= before add, so 3 elements fit (size
0,1,2 when checked)
+ var result = reader.read("[1, 2, 3]");
+ assertNotNull(result);
+ assertEquals(3, ((List<?>) result).size());
+ }
+
+ @Test
+ public void testObjectExceedingMaxElementsThrows() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxElements(2);
+ String json = "{\"a\":1, \"b\":2, \"c\":3}";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed elements (2)"));
+ }
+
+ @Test
+ public void testNestingExceedingMaxDepthThrows() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxDepth(2);
+ String json = "{\"a\":{\"b\":{\"c\":1}}}";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed depth (2)"));
+ assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_DEPTH));
+ }
+
+ @Test
+ public void testArrayNestingExceedingMaxDepthThrows() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxDepth(2);
+ String json = "[[[1]]]";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed depth (2)"));
+ }
+
+ @Test
+ public void testStringAtExactMaxStringLengthAllowed() throws Exception {
+ var reader = new StrutsJSONReader();
+ reader.setMaxStringLength(5);
+ // String "abcde" has length exactly 5, should be allowed (check is >)
+ Object result = reader.read("\"abcde\"");
+ assertEquals("abcde", result);
+ }
+
+ @Test
+ public void testStringExceedingMaxStringLengthThrows() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxStringLength(5);
+ String json = "\"abcdefghij\"";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed length (5)"));
+
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_STRING_LENGTH));
+ }
+
+ @Test
+ public void testObjectKeyExceedingMaxKeyLengthThrowsOnFirstKey() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxKeyLength(3);
+ String json = "{\"longkey\":1}";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed length (3)"));
+
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_KEY_LENGTH));
+ }
+
+ @Test
+ public void testObjectKeyExceedingMaxKeyLengthThrowsOnSubsequentKey() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxKeyLength(3);
+ // First key "a" is within limit, second key "longkey" exceeds it
+ String json = "{\"a\":1, \"longkey\":2}";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed length (3)"));
+
assertTrue(ex.getMessage().contains(JSONConstants.JSON_MAX_KEY_LENGTH));
+ }
+
+ @Test
+ public void testObjectKeyAtExactMaxKeyLengthAllowed() throws Exception {
+ var reader = new StrutsJSONReader();
+ reader.setMaxKeyLength(3);
+ // Key "abc" has length exactly 3, should be allowed (check is >)
+ var result = reader.read("{\"abc\":1}");
+ assertTrue(result instanceof Map);
+ assertEquals(1L, ((Map<?, ?>) result).get("abc"));
+ }
+
+ @Test
+ public void testDefaultLimitsAllowTypicalPayload() throws Exception {
+ var reader = new StrutsJSONReader();
+ var json = "{\"name\":\"test\", \"values\":[1, 2, 3],
\"nested\":{\"key\":\"value\"}}";
+ assertTrue(reader.read(json) instanceof Map);
+ }
+
+ @Test
+ public void testDepthCounterResetsAfterParsing() throws Exception {
+ var reader = new StrutsJSONReader();
+ reader.setMaxDepth(3);
+
+ // First parse: uses depth up to 2
+ assertTrue(reader.read("{\"a\":{\"b\":1}}") instanceof Map);
+
+ // Second parse: should work fine if depth was reset
+ assertTrue(reader.read("{\"a\":{\"b\":1}}") instanceof Map);
+ }
+
+ @Test
+ public void testMixedNestingDepth() {
+ var reader = new StrutsJSONReader();
+ reader.setMaxDepth(2);
+ // array inside object inside array = depth 3, should fail at depth 2
+ String json = "[{\"a\":[1]}]";
+ var ex = assertThrows(JSONException.class, () -> reader.read(json));
+ assertTrue(ex.getMessage().contains("maximum allowed depth"));
+ }
+}
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONWriterTest.java
similarity index 86%
rename from
plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java
rename to
plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONWriterTest.java
index 27b127c0f..acc52dcaa 100644
---
a/plugins/json/src/test/java/org/apache/struts2/json/DefaultJSONWriterTest.java
+++
b/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONWriterTest.java
@@ -22,6 +22,7 @@ import org.apache.struts2.json.annotations.JSONFieldBridge;
import org.apache.struts2.junit.util.TestUtils;
import org.junit.Test;
+import java.net.URI;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.time.Instant;
@@ -43,7 +44,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
-public class DefaultJSONWriterTest {
+public class StrutsJSONWriterTest {
@Test
public void testWrite() throws Exception {
@@ -58,10 +59,10 @@ public class DefaultJSONWriterTest {
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
String json = jsonWriter.write(bean1);
-
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-01.txt"),
json);
+
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-01.txt"),
json);
}
@Test
@@ -83,11 +84,11 @@ public class DefaultJSONWriterTest {
m.put("c", "z");
bean1.setMap(m);
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setIgnoreHierarchy(false);
String json = jsonWriter.write(bean1, null, null, true);
-
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-03.txt"),
json);
+
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-03.txt"),
json);
}
private static class BeanWithMap extends Bean {
@@ -114,13 +115,13 @@ public class DefaultJSONWriterTest {
bean1.setLongField(100);
bean1.setEnumField(AnEnum.ValueA);
bean1.setEnumBean(AnEnumBean.Two);
- bean1.setUrl(new URL("http://www.google.com"));
+ bean1.setUrl(URI.create("https://www.google.com").toURL());
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setIgnoreHierarchy(false);
String json = jsonWriter.write(bean1);
-
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-02.txt"),
json);
+
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-02.txt"),
json);
}
@Test
@@ -139,11 +140,11 @@ public class DefaultJSONWriterTest {
errors.add("Field is required");
bean1.setErrors(errors);
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setIgnoreHierarchy(false);
String json = jsonWriter.write(bean1);
-
TestUtils.assertEquals(DefaultJSONWriter.class.getResource("jsonwriter-write-bean-04.txt"),
json);
+
TestUtils.assertEquals(StrutsJSONWriter.class.getResource("jsonwriter-write-bean-04.txt"),
json);
}
private static class BeanWithList extends Bean {
@@ -178,7 +179,7 @@ public class DefaultJSONWriterTest {
SingleDateBean dateBean = new SingleDateBean();
dateBean.setDate(sdf.parse("2012-12-23 10:10:10 GMT"));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
@@ -193,7 +194,7 @@ public class DefaultJSONWriterTest {
SingleDateBean dateBean = new SingleDateBean();
dateBean.setDate(sdf.parse("2012-12-23 10:10:10 GMT"));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
jsonWriter.setEnumAsBean(false);
jsonWriter.setDateFormatter("MM-dd-yyyy");
String json = jsonWriter.write(dateBean);
@@ -205,7 +206,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setLocalDate(LocalDate.of(2026, 2, 27));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"localDate\":\"2026-02-27\""));
}
@@ -215,7 +216,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setLocalDateTime(LocalDateTime.of(2026, 2, 27, 12, 0, 0));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"localDateTime\":\"2026-02-27T12:00:00\""));
}
@@ -225,7 +226,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setLocalTime(LocalTime.of(12, 0, 0));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"localTime\":\"12:00:00\""));
}
@@ -235,7 +236,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setZonedDateTime(ZonedDateTime.of(2026, 2, 27, 12, 0, 0, 0,
ZoneId.of("Europe/Paris")));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"zonedDateTime\":\"2026-02-27T12:00:00+01:00[Europe\\/Paris]\""));
}
@@ -245,7 +246,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setOffsetDateTime(OffsetDateTime.of(2026, 2, 27, 12, 0, 0, 0,
ZoneOffset.ofHours(1)));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"offsetDateTime\":\"2026-02-27T12:00:00+01:00\""));
}
@@ -255,7 +256,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setInstant(Instant.parse("2026-02-27T11:00:00Z"));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"instant\":\"2026-02-27T11:00:00Z\""));
}
@@ -265,7 +266,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCustomFormatDate(LocalDate.of(2026, 2, 27));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"customFormatDate\":\"27\\/02\\/2026\""));
}
@@ -275,7 +276,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCustomFormatDateTime(LocalDateTime.of(2026, 2, 27, 14, 30));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"customFormatDateTime\":\"27\\/02\\/2026
14:30\""));
}
@@ -284,7 +285,7 @@ public class DefaultJSONWriterTest {
public void testSerializeNullTemporalField() throws Exception {
TemporalBean bean = new TemporalBean();
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean, null, null, true);
assertFalse(json.contains("\"localDate\""));
}
@@ -294,7 +295,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCustomFormatInstant(Instant.parse("2026-02-27T11:00:00Z"));
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"customFormatInstant\":\"2026-02-27
11:00:00\""));
}
@@ -308,7 +309,7 @@ public class DefaultJSONWriterTest {
TemporalBean bean = new TemporalBean();
bean.setCalendar(cal);
- JSONWriter jsonWriter = new DefaultJSONWriter();
+ JSONWriter jsonWriter = new StrutsJSONWriter();
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
String json = jsonWriter.write(bean);
assertTrue(json.contains("\"calendar\":\"2012-12-23T10:10:10\""));
diff --git
a/plugins/json/src/test/resources/org/apache/struts2/json/jsonwriter-write-bean-02.txt
b/plugins/json/src/test/resources/org/apache/struts2/json/jsonwriter-write-bean-02.txt
index 31308a524..09a52d6c5 100644
---
a/plugins/json/src/test/resources/org/apache/struts2/json/jsonwriter-write-bean-02.txt
+++
b/plugins/json/src/test/resources/org/apache/struts2/json/jsonwriter-write-bean-02.txt
@@ -12,5 +12,5 @@
"longField":100,
"objectField":null,
"stringField":"str",
- "url":"http:\/\/www.google.com"
+ "url":"https:\/\/www.google.com"
}
diff --git
a/thoughts/shared/research/2026-03-16-json-plugin-configurable-limits-plan.md
b/thoughts/shared/research/2026-03-16-json-plugin-configurable-limits-plan.md
new file mode 100644
index 000000000..41ac3b716
--- /dev/null
+++
b/thoughts/shared/research/2026-03-16-json-plugin-configurable-limits-plan.md
@@ -0,0 +1,450 @@
+---
+date: 2026-03-16T12:00:00+01:00
+topic: "JSON Plugin Configurable Limits - Implementation Plan"
+tags: [plan, json-plugin, configuration, hardening, WW-5618]
+status: ready
+---
+
+# Implementation Plan: JSON Plugin Configurable Limits
+
+## JIRA Issue
+
+**Ticket:** [WW-5618](https://issues.apache.org/jira/browse/WW-5618)
+
+---
+
+## Key Design Decisions
+
+1. **Instance methods over static:** `JSONUtil.deserialize()` becomes instance
methods. `JSONInterceptor` gets `JSONUtil` injected via Container (it's already
a registered prototype bean). `JSONUtil` gets `JSONReader` and `JSONWriter`
injected via simple `@Inject` (no manual Container lookup).
+
+2. **Naming convention:** Both reader and writer follow the `Struts*` naming
convention for framework implementations. `JSONReader` becomes an interface
with `StrutsJSONReader` as the implementation. `DefaultJSONWriter` is renamed
to `StrutsJSONWriter`. Both registered in `struts-plugin.xml` as default beans.
+
+3. **Bean selection via `BeanSelectionProvider`:** A new
`JSONBeanSelectionProvider` (extending `AbstractBeanSelectionProvider`) uses
the `alias()` mechanism to resolve `JSONReader` and `JSONWriter` beans by
constant-configured name. This replaces the manual two-step
`container.getInstance()` lookup in `JSONUtil.setContainer()` and follows the
same pattern as `VelocityBeanSelectionProvider`. Users can swap implementations
by name, class name, or Spring bean ID.
+
+4. **Backward compatibility:** The current `JSONReader` class is used only
internally — `JSONUtil.deserialize()` calls `new JSONReader()` in a static
method. No external code should be extending it. The static `deserialize()`
methods on `JSONUtil` will be deprecated but kept (delegating to instance
methods) to avoid breaking any direct callers.
+
+5. **`@Inject` pattern:** All injected constants use `String` parameter type
with conversion in the setter body, following the established Struts convention
(e.g., `setDefaultEncoding(String val)` in `JSONInterceptor`).
+
+6. **Limits flow:** `JSONInterceptor` → sets limits on `JSONUtil` instance →
`JSONUtil` passes limits to `JSONReader` before each `deserialize()` call.
Per-action `<param>` overrides on the interceptor take precedence over global
constants.
+
+---
+
+## Implementation Steps
+
+### Step 1: Add constants to `JSONConstants`
+
+**File:**
`plugins/json/src/main/java/org/apache/struts2/json/JSONConstants.java`
+
+Add new constants:
+
+```java
+public static final String JSON_READER = "struts.json.reader";
+public static final String JSON_MAX_ELEMENTS = "struts.json.maxElements";
+public static final String JSON_MAX_DEPTH = "struts.json.maxDepth";
+public static final String JSON_MAX_LENGTH = "struts.json.maxLength";
+public static final String JSON_MAX_STRING_LENGTH =
"struts.json.maxStringLength";
+public static final String JSON_MAX_KEY_LENGTH = "struts.json.maxKeyLength";
+```
+
+**Expected outcome:** Constants available for injection and XML configuration.
+
+---
+
+### Step 2: Update `JSONConstantConfig`
+
+**File:**
`plugins/json/src/main/java/org/apache/struts2/json/config/entities/JSONConstantConfig.java`
+
+Add fields, getters, setters, and `getAllAsStringsMap()` entries for each new
constant. Follow the existing pattern (`jsonWriter`, `jsonDateFormat`):
+
+```java
+private BeanConfig jsonReader;
+private Integer jsonMaxElements;
+private Integer jsonMaxDepth;
+private Integer jsonMaxLength;
+private Integer jsonMaxStringLength;
+private Integer jsonMaxKeyLength;
+```
+
+**Expected outcome:** New constants wired into the ConstantConfig system.
+
+---
+
+### Step 3: Extract `JSONReader` interface and create `StrutsJSONReader`
+
+**Current file:**
`plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java`
+**New file:**
`plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java`
+
+#### 3a: Create `JSONReader` interface
+
+Replace the current class with an interface:
+
+```java
+public interface JSONReader {
+
+ int DEFAULT_MAX_ELEMENTS = 10_000;
+ int DEFAULT_MAX_DEPTH = 64;
+ int DEFAULT_MAX_STRING_LENGTH = 262_144; // 256KB
+ int DEFAULT_MAX_KEY_LENGTH = 512;
+
+ Object read(String string) throws JSONException;
+
+ void setMaxElements(int maxElements);
+ void setMaxDepth(int maxDepth);
+ void setMaxStringLength(int maxStringLength);
+ void setMaxKeyLength(int maxKeyLength);
+}
+```
+
+#### 3b: Create `StrutsJSONReader` implementation
+
+Rename current `JSONReader` class to `StrutsJSONReader implements JSONReader`.
Add:
+
+- Limit fields with defaults from the interface constants
+- A `depth` counter field, incremented on entry to `array()`/`object()`,
decremented on exit (use try/finally)
+- In `array()`: check `ret.size() >= maxElements` before `ret.add()`, throw
`JSONException` if exceeded
+- In `object()`: check `ret.size() >= maxElements` before `ret.put()`, throw
`JSONException` if exceeded
+- In `read()`: check `depth >= maxDepth` before calling `array()`/`object()`,
throw `JSONException` if exceeded
+- In `string()`: check `buf.length() >= maxStringLength` in the character
loop, throw `JSONException` if exceeded
+- In `object()`: check key length against `maxKeyLength` after reading key
string
+
+Error messages should be clear and include the limit value, e.g.:
+`"JSON array exceeds maximum allowed elements (10000). Use
struts.json.maxElements to increase the limit."`
+
+**Expected outcome:** `JSONReader` is an interface; `StrutsJSONReader`
enforces configurable bounds.
+
+---
+
+### Step 4: Rename `DefaultJSONWriter` to `StrutsJSONWriter`
+
+**Current file:**
`plugins/json/src/main/java/org/apache/struts2/json/DefaultJSONWriter.java`
+**New file:**
`plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java`
+
+Rename the class from `DefaultJSONWriter` to `StrutsJSONWriter`. This is a
mechanical rename — the `JSONWriter` interface stays unchanged.
+
+Changes required:
+- Rename class file and class declaration
+- Update `struts-plugin.xml` bean registration:
`class="org.apache.struts2.json.StrutsJSONWriter"`
+- Update all test references (~40 occurrences across `JSONResultTest.java`,
`DefaultJSONWriterTest.java`, `JSONUtilTest.java`, `JSONEnumTest.java`)
+- Rename `DefaultJSONWriterTest.java` to `StrutsJSONWriterTest.java`
+- Update resource references (e.g., `DefaultJSONWriter.class.getResource(...)`
→ `StrutsJSONWriter.class.getResource(...)`)
+
+**Expected outcome:** Writer follows the same `Struts*` naming convention as
the reader. `JSONWriter` interface is unchanged — no impact on custom
implementations.
+
+---
+
+### Step 5: Create `JSONBeanSelectionProvider`
+
+**New file:**
`plugins/json/src/main/java/org/apache/struts2/json/JSONBeanSelectionProvider.java`
+
+Create a bean selection provider following the `VelocityBeanSelectionProvider`
pattern:
+
+```java
+package org.apache.struts2.json;
+
+import org.apache.struts2.config.AbstractBeanSelectionProvider;
+import org.apache.struts2.config.ConfigurationException;
+import org.apache.struts2.inject.ContainerBuilder;
+import org.apache.struts2.inject.Scope;
+import org.apache.struts2.util.location.LocatableProperties;
+
+public class JSONBeanSelectionProvider extends AbstractBeanSelectionProvider {
+
+ @Override
+ public void register(ContainerBuilder builder, LocatableProperties props)
+ throws ConfigurationException {
+ alias(JSONReader.class, JSONConstants.JSON_READER, builder, props,
Scope.PROTOTYPE);
+ alias(JSONWriter.class, JSONConstants.JSON_WRITER, builder, props,
Scope.PROTOTYPE);
+ }
+}
+```
+
+This uses the standard `alias()` mechanism from
`AbstractBeanSelectionProvider` which:
+1. Reads the constant value (e.g., `struts.json.reader` → `"struts"`)
+2. Finds the bean registered under that name
+3. Aliases it to `Container.DEFAULT_NAME` so plain `@Inject` resolves it
+4. Falls back to class name loading or Spring bean ID delegation if the name
isn't a registered bean
+
+**Expected outcome:** `JSONReader` and `JSONWriter` beans are selectable via
constants using the standard Struts bean aliasing mechanism. Users can swap
implementations by bean name, fully qualified class name, or Spring bean ID.
+
+---
+
+### Step 6: Update `JSONUtil` to use instance methods and simple `@Inject`
+
+**File:** `plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java`
+
+#### 6a: Replace manual Container lookup with simple `@Inject`
+
+Remove the existing `setContainer()` method with its manual two-step
resolution. Replace with direct injection (the `BeanSelectionProvider` aliases
handle the name resolution):
+
+```java
+private JSONReader reader;
+private JSONWriter writer;
+
+@Inject
+public void setReader(JSONReader reader) {
+ this.reader = reader;
+}
+
+@Inject
+public void setWriter(JSONWriter writer) {
+ this.writer = writer;
+}
+
+public JSONReader getReader() {
+ return reader;
+}
+```
+
+#### 6b: Add instance `deserialize()` methods with `maxLength` check
+
+```java
+public Object deserialize(Reader reader, int maxLength) throws JSONException {
+ BufferedReader bufferReader = new BufferedReader(reader);
+ String line;
+ StringBuilder buffer = new StringBuilder();
+ try {
+ while ((line = bufferReader.readLine()) != null) {
+ buffer.append(line);
+ if (buffer.length() > maxLength) {
+ throw new JSONException("JSON input exceeds maximum allowed
length ("
+ + maxLength + "). Use struts.json.maxLength to increase
the limit.");
+ }
+ }
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ return this.reader.read(buffer.toString());
+}
+```
+
+#### 6c: Deprecate static `deserialize()` methods
+
+Keep existing static methods but mark `@Deprecated` and delegate internally
(create a default `StrutsJSONReader` for backward compatibility):
+
+```java
+@Deprecated
+public static Object deserialize(String json) throws JSONException {
+ StrutsJSONReader reader = new StrutsJSONReader();
+ return reader.read(json);
+}
+```
+
+**Expected outcome:** `JSONUtil` uses simple `@Inject` for both reader and
writer; limits flow through instance methods; static API preserved but
deprecated.
+
+---
+
+### Step 7: Wire limits into `JSONInterceptor`
+
+**File:**
`plugins/json/src/main/java/org/apache/struts2/json/JSONInterceptor.java`
+
+#### 7a: Inject `JSONUtil` instance instead of static access
+
+```java
+private JSONUtil jsonUtil;
+
+@Inject
+public void setJsonUtil(JSONUtil jsonUtil) {
+ this.jsonUtil = jsonUtil;
+}
+```
+
+#### 7b: Add limit fields with `@Inject` from constants
+
+```java
+private int maxElements = JSONReader.DEFAULT_MAX_ELEMENTS;
+private int maxDepth = JSONReader.DEFAULT_MAX_DEPTH;
+private int maxLength = 2_097_152; // 2MB
+private int maxStringLength = JSONReader.DEFAULT_MAX_STRING_LENGTH;
+private int maxKeyLength = JSONReader.DEFAULT_MAX_KEY_LENGTH;
+
+@Inject(value = JSONConstants.JSON_MAX_ELEMENTS, required = false)
+public void setMaxElements(String maxElements) {
+ this.maxElements = Integer.parseInt(maxElements);
+}
+
+@Inject(value = JSONConstants.JSON_MAX_DEPTH, required = false)
+public void setMaxDepth(String maxDepth) {
+ this.maxDepth = Integer.parseInt(maxDepth);
+}
+
+// ... same pattern for maxLength, maxStringLength, maxKeyLength
+```
+
+#### 7c: Update `intercept()` to use instance `jsonUtil` and pass limits
+
+Replace the static call:
+```java
+// Before:
+Object obj = JSONUtil.deserialize(request.getReader());
+
+// After:
+jsonUtil.getReader().setMaxElements(maxElements);
+jsonUtil.getReader().setMaxDepth(maxDepth);
+jsonUtil.getReader().setMaxStringLength(maxStringLength);
+jsonUtil.getReader().setMaxKeyLength(maxKeyLength);
+Object obj = jsonUtil.deserialize(request.getReader(), maxLength);
+```
+
+Do the same for the SMD deserialization path (line 136).
+
+Note: Since `JSONUtil` is prototype-scoped, the reader instance is
per-interceptor invocation when injected properly. But to be safe, limits
should be set before each deserialization call.
+
+#### 7d: Also update `JSONResult` and `JSONValidationInterceptor` if they use
static `JSONUtil.deserialize()`
+
+Check these classes and update them to use injected `JSONUtil` if they call
the static deserialize methods.
+
+**Expected outcome:** Limits flow from interceptor → JSONUtil → JSONReader per
request. Configurable globally and per-action.
+
+---
+
+### Step 8: Register beans and defaults in `struts-plugin.xml`
+
+**File:** `plugins/json/src/main/resources/struts-plugin.xml`
+
+```xml
+<!-- Bean selection provider for JSONReader/JSONWriter aliasing -->
+<bean-selection name="jsonBeans"
+ class="org.apache.struts2.json.JSONBeanSelectionProvider"/>
+
+<!-- JSONWriter bean (renamed from DefaultJSONWriter) -->
+<bean type="org.apache.struts2.json.JSONWriter" name="struts"
+ class="org.apache.struts2.json.StrutsJSONWriter" scope="prototype"/>
+<constant name="struts.json.writer" value="struts"/>
+
+<!-- JSONReader bean -->
+<bean type="org.apache.struts2.json.JSONReader" name="struts"
+ class="org.apache.struts2.json.StrutsJSONReader" scope="prototype"/>
+<constant name="struts.json.reader" value="struts"/>
+
+<!-- JSONUtil (prototype — not thread-safe) -->
+<bean class="org.apache.struts2.json.JSONUtil" scope="prototype"/>
+
+<!-- Default limits -->
+<constant name="struts.json.maxElements" value="10000"/>
+<constant name="struts.json.maxDepth" value="64"/>
+<constant name="struts.json.maxLength" value="2097152"/>
+<constant name="struts.json.maxStringLength" value="262144"/>
+<constant name="struts.json.maxKeyLength" value="512"/>
+```
+
+**Expected outcome:** Sensible defaults applied out-of-the-box; all values
overridable. Users can swap implementations via constants.
+
+---
+
+### Step 9: Write tests
+
+**File:**
`plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONReaderTest.java`
(new, for the implementation)
+**File:**
`plugins/json/src/test/java/org/apache/struts2/json/JSONReaderTest.java`
(existing, keep for backward compat of deprecated static API)
+
+#### Unit tests for `StrutsJSONReader`:
+- [ ] Array with elements under limit parses successfully
+- [ ] Array exceeding `maxElements` throws `JSONException` with descriptive
message
+- [ ] Object exceeding `maxElements` throws `JSONException`
+- [ ] Nesting within `maxDepth` parses successfully
+- [ ] Nesting exceeding `maxDepth` throws `JSONException`
+- [ ] String within `maxStringLength` parses successfully
+- [ ] String exceeding `maxStringLength` throws `JSONException`
+- [ ] Object key exceeding `maxKeyLength` throws `JSONException`
+- [ ] Default limits allow typical JSON payloads (regression)
+- [ ] Custom limits via setters work correctly
+- [ ] Depth counter resets correctly after parsing (no state leakage)
+
+#### Unit tests for `JSONUtil` instance methods:
+- [ ] Input exceeding `maxLength` throws `JSONException` before parsing begins
+- [ ] Input within `maxLength` parses successfully
+- [ ] Deprecated static methods still work (backward compat)
+
+#### Integration test (if feasible):
+- [ ] Per-action `<param>` override applies different limits than global
default
+
+**Expected outcome:** All limit boundaries tested; existing tests continue to
pass.
+
+---
+
+### Step 10: Update documentation
+
+- Update JSON plugin documentation page to describe new configuration options
+- Add migration notes: mention that new defaults may reject unusually large
payloads
+- Document per-action override pattern with XML example
+- Document custom `JSONReader`/`JSONWriter` implementation pattern
+
+---
+
+## Files Modified (Summary)
+
+| File | Change |
+|------|--------|
+| `JSONConstants.java` | Add 6 new constants |
+| `JSONConstantConfig.java` | Add fields, getters, setters for new constants |
+| `JSONReader.java` | **Rewrite** — becomes an interface with limit setters
and defaults |
+| `StrutsJSONReader.java` | **New** — implementation with depth tracking and
bounds checks |
+| `DefaultJSONWriter.java` | **Rename** → `StrutsJSONWriter.java` (class name
+ file) |
+| `JSONBeanSelectionProvider.java` | **New** — bean aliasing for reader/writer
via constants |
+| `JSONUtil.java` | Replace manual Container lookup with `@Inject`, add
instance `deserialize()`, deprecate static ones |
+| `JSONInterceptor.java` | Inject `JSONUtil`, add `@Inject` + setter methods
for limits |
+| `struts-plugin.xml` | Add `bean-selection`, register `StrutsJSONReader`,
rename writer bean, add defaults |
+| `StrutsJSONReaderTest.java` | **New** — test cases for all limits |
+| `DefaultJSONWriterTest.java` | **Rename** → `StrutsJSONWriterTest.java`,
update all references |
+| `JSONResultTest.java` | Update `DefaultJSONWriter` → `StrutsJSONWriter` (~25
occurrences) |
+| `JSONUtilTest.java` | Update `DefaultJSONWriter` → `StrutsJSONWriter`
references |
+| `JSONEnumTest.java` | Update `DefaultJSONWriter` → `StrutsJSONWriter`
references |
+| `JSONReaderTest.java` | Keep existing tests, verify backward compat |
+
+## Configuration Examples
+
+### Global defaults (struts.xml)
+```xml
+<constant name="struts.json.maxElements" value="50000"/>
+<constant name="struts.json.maxDepth" value="32"/>
+<constant name="struts.json.maxLength" value="5242880"/> <!-- 5MB -->
+```
+
+### Per-action override
+```xml
+<action name="importData" class="com.example.ImportAction">
+ <interceptor-ref name="json">
+ <param name="maxElements">100000</param>
+ <param name="maxDepth">10</param>
+ <param name="maxLength">10485760</param> <!-- 10MB -->
+ </interceptor-ref>
+ <result type="json"/>
+</action>
+```
+
+### Custom JSONReader implementation
+```xml
+<bean type="org.apache.struts2.json.JSONReader" name="custom"
+ class="com.example.MyJSONReader" scope="prototype"/>
+<constant name="struts.json.reader" value="custom"/>
+```
+
+### Custom JSONWriter implementation
+```xml
+<bean type="org.apache.struts2.json.JSONWriter" name="custom"
+ class="com.example.MyJSONWriter" scope="prototype"/>
+<constant name="struts.json.writer" value="custom"/>
+```
+
+### Custom implementation via Spring bean ID
+```xml
+<!-- Works because BeanSelectionProvider falls back to ObjectFactory/Spring -->
+<constant name="struts.json.reader" value="mySpringJsonReaderBean"/>
+```
+
+## Risks and Considerations
+
+1. **Breaking change — `JSONReader` becomes an interface:** Code that directly
instantiated `new JSONReader()` will break. However, `JSONReader` was only used
internally by `JSONUtil.deserialize()` static methods. External code calling
the static API will still work via the deprecated path. Risk: **low**.
+
+2. **Breaking change — static `deserialize()` deprecated:** Callers using
`JSONUtil.deserialize(String)` or `JSONUtil.deserialize(Reader)` statically
will get deprecation warnings but the methods still work. They just won't have
limit enforcement. Risk: **none** (functional).
+
+3. **New defaults may reject large payloads:** A payload with >10,000 array
elements or >64 nesting depth will now be rejected. Mitigation: defaults are
generous for typical use; clear error messages include the constant name to
override. Risk: **low**.
+
+4. **Performance:** Bounds checks add negligible overhead (integer comparison
per element/depth increment).
+
+5. **Breaking change — `DefaultJSONWriter` renamed to `StrutsJSONWriter`:**
Code that directly references `DefaultJSONWriter` (e.g., `new
DefaultJSONWriter()` in tests or custom code) will break. However,
`DefaultJSONWriter` was never part of the public API — users should reference
the `JSONWriter` interface. Bean registration in `struts-plugin.xml` is
internal. Risk: **low** — affects custom code that bypasses the Container.
+
+6. **Thread safety:** `StrutsJSONReader` is not thread-safe (instance fields
for parser state). Registered as `scope="prototype"` — same as
`StrutsJSONWriter`. Each deserialization gets a fresh instance.
+
+7. **Prototype scope and limit setting:** Since `JSONUtil` is prototype-scoped
and holds a prototype `JSONReader`, the interceptor sets limits on the reader
before each deserialization. This is safe because each interceptor invocation
gets its own `JSONUtil` instance from the Container.
+
+8. **`BeanSelectionProvider` ordering:** The `<bean-selection>` element in
`struts-plugin.xml` must appear so that the beans it references
(`StrutsJSONReader`, `StrutsJSONWriter`) are already registered. Since `<bean>`
elements are processed before `<bean-selection>`, this works naturally.