diff --git a/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java
index 530a479cc6..96593d2b97 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONReader.java
@@ -30,6 +30,13 @@
* Deserializes an object from a JSON string with configurable limits
* to prevent denial-of-service attacks via malicious payloads.
*
+ *
+ *
+ * A single StrutsJSONReader instance is shared across all concurrent requests handled by a given
+ * JSONInterceptor (it is injected once, not created per request), so the cursor, token buffer and
+ * nesting depth of an in-progress parse are kept in a {@link ThreadLocal}, not instance fields --
+ * otherwise two concurrent {@link #read(String)} calls would corrupt each other's parse state.
+ *
*/
public class StrutsJSONReader implements JSONReader {
private static final Object OBJECT_END = new Object();
@@ -47,16 +54,20 @@ public class StrutsJSONReader implements JSONReader {
't', '\t'
);
- private CharacterIterator it;
- private char c;
- private Object token;
- private final StringBuilder buf = new StringBuilder();
+ private static final class ParseState {
+ private CharacterIterator it;
+ private char c;
+ private Object token;
+ private final StringBuilder buf = new StringBuilder();
+ private int depth;
+ }
+
+ private static final ThreadLocal PARSE_STATE = new ThreadLocal<>();
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) {
@@ -79,92 +90,99 @@ public void setMaxKeyLength(int maxKeyLength) {
}
protected char next() {
- this.c = this.it.next();
+ ParseState state = PARSE_STATE.get();
+ state.c = state.it.next();
- return this.c;
+ return state.c;
}
protected void skipWhiteSpace() {
- while (Character.isWhitespace(this.c)) {
+ while (Character.isWhitespace(PARSE_STATE.get().c)) {
this.next();
}
}
@Override
public Object read(String string) throws JSONException {
- this.it = new StringCharacterIterator(string);
- this.c = this.it.first();
- this.depth = 0;
-
- return this.read();
+ ParseState state = new ParseState();
+ state.it = new StringCharacterIterator(string);
+ state.c = state.it.first();
+ PARSE_STATE.set(state);
+ try {
+ return this.read();
+ } finally {
+ PARSE_STATE.remove();
+ }
}
protected Object read() throws JSONException {
+ ParseState state = PARSE_STATE.get();
Object ret;
this.skipWhiteSpace();
- if (this.c == '"') {
+ if (state.c == '"') {
this.next();
ret = this.string('"');
- } else if (this.c == '\'') {
+ } else if (state.c == '\'') {
this.next();
ret = this.string('\'');
- } else if (this.c == '[') {
+ } else if (state.c == '[') {
this.next();
ret = this.array();
- } else if (this.c == ']') {
+ } else if (state.c == ']') {
ret = ARRAY_END;
this.next();
- } else if (this.c == ',') {
+ } else if (state.c == ',') {
ret = COMMA;
this.next();
- } else if (this.c == '{') {
+ } else if (state.c == '{') {
this.next();
ret = this.object();
- } else if (this.c == '}') {
+ } else if (state.c == '}') {
ret = OBJECT_END;
this.next();
- } else if (this.c == ':') {
+ } else if (state.c == ':') {
ret = COLON;
this.next();
- } else if ((this.c == 't') && (this.next() == 'r') && (this.next() == 'u') && (this.next() == 'e')) {
+ } else if ((state.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')
+ } else if ((state.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')) {
+ } else if ((state.c == 'n') && (this.next() == 'u') && (this.next() == 'l') && (this.next() == 'l')) {
ret = null;
this.next();
- } else if (Character.isDigit(this.c) || (this.c == '-')) {
+ } else if (Character.isDigit(state.c) || (state.c == '-')) {
ret = this.number();
} else {
throw buildInvalidInputException();
}
- this.token = ret;
+ state.token = ret;
return ret;
}
protected Map object() throws JSONException {
- if (this.depth >= this.maxDepth) {
+ ParseState state = PARSE_STATE.get();
+ if (state.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++;
+ state.depth++;
try {
Map ret = new HashMap<>();
Object next = this.read();
if (next != OBJECT_END) {
String key = (String) next;
validateKeyLength(key);
- while (this.token != OBJECT_END) {
+ while (state.token != OBJECT_END) {
this.read(); // should be a colon
- if (this.token != OBJECT_END) {
+ if (state.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.");
@@ -187,7 +205,7 @@ protected Map object() throws JSONException {
return ret;
} finally {
- this.depth--;
+ state.depth--;
}
}
@@ -199,21 +217,22 @@ private void validateKeyLength(String key) throws JSONException {
}
protected JSONException buildInvalidInputException() {
- return new JSONException("Input string is not well formed JSON (invalid char " + this.c + ")");
+ return new JSONException("Input string is not well formed JSON (invalid char " + PARSE_STATE.get().c + ")");
}
protected List