Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ public class JSONConstants {
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";

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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ public class JSONInterceptor extends AbstractInterceptor {
private String jsonContentType = "application/json";
private String jsonRpcContentType = "application/json-rpc";

private JSONUtil jsonUtil = new JSONUtil();
private int maxElements = 10000;
private int maxDepth = 64;
private int maxLength = 2097152;
private int maxStringLength = 262144;
private int maxKeyLength = 512;

@SuppressWarnings("unchecked")
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
Expand All @@ -91,7 +98,8 @@ public String intercept(ActionInvocation invocation) throws Exception {

if (jsonContentType.equalsIgnoreCase(requestContentType)) {
// load JSON object
Object obj = JSONUtil.deserialize(request.getReader());
Object obj = jsonUtil.deserializeInput(request.getReader(), maxLength, maxElements, maxDepth,
maxStringLength, maxKeyLength);

// JSON array (this.root cannot be null in this case)
if(obj instanceof List && this.root != null) {
Expand Down Expand Up @@ -133,7 +141,8 @@ public String intercept(ActionInvocation invocation) throws Exception {
Object result;
if (this.enableSMD) {
// load JSON object
Object obj = JSONUtil.deserialize(request.getReader());
Object obj = jsonUtil.deserializeInput(request.getReader(), maxLength, maxElements, maxDepth,
maxStringLength, maxKeyLength);

if (obj instanceof Map) {
Map smd = (Map) obj;
Expand Down Expand Up @@ -168,9 +177,8 @@ public String intercept(ActionInvocation invocation) throws Exception {
result = rpcResponse;
}

JSONUtil jsonUtil = invocation.getInvocationContext().getContainer().getInstance(JSONUtil.class);

String json = jsonUtil.serialize(result, excludeProperties, getIncludeProperties(),
JSONUtil smdJsonUtil = invocation.getInvocationContext().getContainer().getInstance(JSONUtil.class);
String json = smdJsonUtil.serialize(result, excludeProperties, getIncludeProperties(),
ignoreHierarchy, excludeNullProperties);
json = addCallbackIfApplicable(request, json);
boolean writeGzip = enableGZIP && JSONUtil.isGzipInRequest(request);
Expand Down Expand Up @@ -423,6 +431,36 @@ public void setDevMode(String mode) {
setDebug(BooleanUtils.toBoolean(mode));
}

@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);
}

/**
* Sets a comma-delimited list of regular expressions to match properties
* that should be excluded from the JSON output.
Expand Down
125 changes: 93 additions & 32 deletions plugins/json/src/main/java/org/apache/struts2/json/JSONReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,38 @@ public class JSONReader {
escapes.put('t', '\t');
}

private static final int DEFAULT_MAX_ELEMENTS = 10000;
private static final int DEFAULT_MAX_DEPTH = 64;
private static final int DEFAULT_MAX_STRING_LENGTH = 262144;
private static final int DEFAULT_MAX_KEY_LENGTH = 512;

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;

private CharacterIterator it;
private char c;
private Object token;
private StringBuilder buf = new StringBuilder();

public void setMaxElements(int maxElements) {
this.maxElements = maxElements;
}

public void setMaxDepth(int maxDepth) {
this.maxDepth = maxDepth;
}

public void setMaxStringLength(int maxStringLength) {
this.maxStringLength = maxStringLength;
}

public void setMaxKeyLength(int maxKeyLength) {
this.maxKeyLength = maxKeyLength;
}

protected char next() {
this.c = this.it.next();

Expand Down Expand Up @@ -124,29 +151,51 @@ protected Object read() throws JSONException {

@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();
this.depth++;
if (this.depth > this.maxDepth) {
throw new JSONException("JSON object depth exceeds maximum allowed depth of " + this.maxDepth);
}
try {
Map ret = new HashMap();
Object next = this.read();
if (next != OBJECT_END) {
String key = (String) next;
while (this.token != OBJECT_END) {
validateKeyLength(key);
this.read(); // should be a colon

if (this.token != OBJECT_END) {
ret.put(key, this.read());
validateElementCount(ret.size());

if (this.read() == COMMA) {
Object name = this.read();

if (name instanceof String) {
key = (String) name;
} else
throw buildInvalidInputException();
}
}
}
}

return ret;
} finally {
this.depth--;
}
}

return ret;
private void validateKeyLength(String key) throws JSONException {
if (key != null && key.length() > this.maxKeyLength) {
throw new JSONException("JSON key length " + key.length() + " exceeds maximum allowed length of " + this.maxKeyLength);
}
}

private void validateElementCount(int count) throws JSONException {
if (count > this.maxElements) {
throw new JSONException("JSON element count exceeds maximum allowed count of " + this.maxElements);
}
}

protected JSONException buildInvalidInputException() {
Expand All @@ -156,21 +205,30 @@ protected JSONException buildInvalidInputException() {

@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();
}
this.depth++;
if (this.depth > this.maxDepth) {
throw new JSONException("JSON array depth exceeds maximum allowed depth of " + this.maxDepth);
}
try {
List ret = new ArrayList();
Object value = this.read();

while (this.token != ARRAY_END) {
ret.add(value);
validateElementCount(ret.size());

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 {
Expand Down Expand Up @@ -215,7 +273,7 @@ protected Object number() throws JSONException {
}
}

protected Object string(char quote) {
protected Object string(char quote) throws JSONException {
this.buf.setLength(0);

while ((this.c != quote) && (this.c != CharacterIterator.DONE)) {
Expand All @@ -234,6 +292,9 @@ protected Object string(char quote) {
} else {
this.add();
}
if (this.buf.length() > this.maxStringLength) {
throw new JSONException("JSON string length exceeds maximum allowed length of " + this.maxStringLength);
}
}

this.next();
Expand Down
38 changes: 38 additions & 0 deletions plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,44 @@ public static Object deserialize(Reader reader) throws JSONException {
return deserialize(buffer.toString());
}

/**
* Deserializes a JSON object from a Reader with configurable limits to prevent DoS attacks.
*
* @param reader Reader to read JSON string from
* @param maxLength maximum allowed input length in characters
* @param maxElements maximum allowed number of elements in a single object/array
* @param maxDepth maximum allowed nesting depth
* @param maxStringLength maximum allowed string value length
* @param maxKeyLength maximum allowed key length
* @return deserialized object
* @throws JSONException when limits are exceeded or input is malformed
*/
public Object deserializeInput(Reader reader, int maxLength, int maxElements, int maxDepth,
int maxStringLength, int maxKeyLength) throws JSONException {
BufferedReader bufferReader = new BufferedReader(reader);
StringBuilder buffer = new StringBuilder();
String line;

try {
while ((line = bufferReader.readLine()) != null) {
buffer.append(line);
if (buffer.length() > maxLength) {
throw new JSONException("JSON input length exceeds maximum allowed length of " + maxLength);
}
}
} catch (IOException e) {
throw new JSONException(e);
}

JSONReader jsonReader = new JSONReader();
jsonReader.setMaxElements(maxElements);
jsonReader.setMaxDepth(maxDepth);
jsonReader.setMaxStringLength(maxStringLength);
jsonReader.setMaxKeyLength(maxKeyLength);

return jsonReader.read(buffer.toString());
}

public static void writeJSONToResponse(SerializationParams serializationParams) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
if (StringUtils.isNotBlank(serializationParams.getSerializedJSON()))
Expand Down
5 changes: 5 additions & 0 deletions plugins/json/src/main/resources/struts-plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<bean type="org.apache.struts2.json.JSONWriter" name="struts" class="org.apache.struts2.json.DefaultJSONWriter"
scope="prototype"/>
<constant name="struts.json.writer" 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"/>
<!-- TODO: Make DefaultJSONWriter thread-safe to remove "prototype"s -->
<bean class="org.apache.struts2.json.JSONUtil" scope="prototype"/>

Expand Down
Loading
Loading