Skip to content

Commit 401495a

Browse files
authored
Merge pull request stleary#720 from cleydyr/issue-708
Limit the XML nesting depth for CVE-2022-45688
2 parents 5920eca + 448e204 commit 401495a

File tree

5 files changed

+135
-4
lines changed

5 files changed

+135
-4
lines changed

src/main/java/org/json/XML.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ public static void noSpace(String string) throws JSONException {
232232
* @return true if the close tag is processed.
233233
* @throws JSONException
234234
*/
235-
private static boolean parse(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config)
235+
private static boolean parse(XMLTokener x, JSONObject context, String name, XMLParserConfiguration config, int currentNestingDepth)
236236
throws JSONException {
237237
char c;
238238
int i;
@@ -402,7 +402,11 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, XMLP
402402

403403
} else if (token == LT) {
404404
// Nested element
405-
if (parse(x, jsonObject, tagName, config)) {
405+
if (currentNestingDepth == config.getMaxNestingDepth()) {
406+
throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached");
407+
}
408+
409+
if (parse(x, jsonObject, tagName, config, currentNestingDepth + 1)) {
406410
if (config.getForceList().contains(tagName)) {
407411
// Force the value to be an array
408412
if (jsonObject.length() == 0) {
@@ -655,7 +659,7 @@ public static JSONObject toJSONObject(Reader reader, XMLParserConfiguration conf
655659
while (x.more()) {
656660
x.skipPast("<");
657661
if(x.more()) {
658-
parse(x, jo, null, config);
662+
parse(x, jo, null, config, 0);
659663
}
660664
}
661665
return jo;

src/main/java/org/json/XMLParserConfiguration.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616
*/
1717
@SuppressWarnings({""})
1818
public class XMLParserConfiguration {
19+
/**
20+
* Used to indicate there's no defined limit to the maximum nesting depth when parsing a XML
21+
* document to JSON.
22+
*/
23+
public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1;
24+
25+
/**
26+
* The default maximum nesting depth when parsing a XML document to JSON.
27+
*/
28+
public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512;
29+
1930
/** Original Configuration of the XML Parser. */
2031
public static final XMLParserConfiguration ORIGINAL
2132
= new XMLParserConfiguration();
@@ -54,6 +65,12 @@ public class XMLParserConfiguration {
5465
*/
5566
private Set<String> forceList;
5667

68+
/**
69+
* When parsing the XML into JSON, specifies the tags whose values should be converted
70+
* to arrays
71+
*/
72+
private int maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH;
73+
5774
/**
5875
* Default parser configuration. Does not keep strings (tries to implicitly convert
5976
* values), and the CDATA Tag Name is "content".
@@ -297,4 +314,33 @@ public XMLParserConfiguration withForceList(final Set<String> forceList) {
297314
newConfig.forceList = Collections.unmodifiableSet(cloneForceList);
298315
return newConfig;
299316
}
317+
318+
/**
319+
* The maximum nesting depth that the parser will descend before throwing an exception
320+
* when parsing the XML into JSON.
321+
* @return the maximum nesting depth set for this configuration
322+
*/
323+
public int getMaxNestingDepth() {
324+
return maxNestingDepth;
325+
}
326+
327+
/**
328+
* Defines the maximum nesting depth that the parser will descend before throwing an exception
329+
* when parsing the XML into JSON. The default max nesting depth is 512, which means the parser
330+
* will go as deep as the maximum call stack size allows. Using any negative value as a
331+
* parameter is equivalent to setting no limit to the nesting depth.
332+
* @param maxNestingDepth the maximum nesting depth allowed to the XML parser
333+
* @return The existing configuration will not be modified. A new configuration is returned.
334+
*/
335+
public XMLParserConfiguration withMaxNestingDepth(int maxNestingDepth) {
336+
XMLParserConfiguration newConfig = this.clone();
337+
338+
if (maxNestingDepth > UNDEFINED_MAXIMUM_NESTING_DEPTH) {
339+
newConfig.maxNestingDepth = maxNestingDepth;
340+
} else {
341+
newConfig.maxNestingDepth = UNDEFINED_MAXIMUM_NESTING_DEPTH;
342+
}
343+
344+
return newConfig;
345+
}
300346
}

src/test/java/org/json/junit/JSONArrayTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import static org.junit.Assert.assertEquals;
88
import static org.junit.Assert.assertFalse;
9-
import static org.junit.Assert.assertNotEquals;
109
import static org.junit.Assert.assertNotNull;
1110
import static org.junit.Assert.assertNull;
1211
import static org.junit.Assert.assertTrue;

src/test/java/org/json/junit/XMLConfigurationTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,29 @@ public void testEmptyTagForceList() {
10511051

10521052
Util.compareActualVsExpectedJsonObjects(jsonObject, expetedJsonObject);
10531053
}
1054+
1055+
@Test
1056+
public void testMaxNestingDepthIsSet() {
1057+
XMLParserConfiguration xmlParserConfiguration = XMLParserConfiguration.ORIGINAL;
1058+
1059+
assertEquals(xmlParserConfiguration.getMaxNestingDepth(), XMLParserConfiguration.DEFAULT_MAXIMUM_NESTING_DEPTH);
1060+
1061+
xmlParserConfiguration = xmlParserConfiguration.withMaxNestingDepth(42);
1062+
1063+
assertEquals(xmlParserConfiguration.getMaxNestingDepth(), 42);
1064+
1065+
xmlParserConfiguration = xmlParserConfiguration.withMaxNestingDepth(0);
1066+
1067+
assertEquals(xmlParserConfiguration.getMaxNestingDepth(), 0);
1068+
1069+
xmlParserConfiguration = xmlParserConfiguration.withMaxNestingDepth(-31415926);
1070+
1071+
assertEquals(xmlParserConfiguration.getMaxNestingDepth(), XMLParserConfiguration.UNDEFINED_MAXIMUM_NESTING_DEPTH);
1072+
1073+
xmlParserConfiguration = xmlParserConfiguration.withMaxNestingDepth(Integer.MIN_VALUE);
1074+
1075+
assertEquals(xmlParserConfiguration.getMaxNestingDepth(), XMLParserConfiguration.UNDEFINED_MAXIMUM_NESTING_DEPTH);
1076+
}
10541077

10551078
/**
10561079
* Convenience method, given an input string and expected result,

src/test/java/org/json/junit/XMLTest.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,65 @@ public void testIndentComplicatedJsonObjectWithArrayAndWithConfig(){
12471247
fail("file writer error: " +e.getMessage());
12481248
}
12491249
}
1250+
1251+
@Test
1252+
public void testMaxNestingDepthOf42IsRespected() {
1253+
final String wayTooLongMalformedXML = new String(new char[6000]).replace("\0", "<a>");
1254+
1255+
final int maxNestingDepth = 42;
1256+
1257+
try {
1258+
XML.toJSONObject(wayTooLongMalformedXML, XMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
1259+
1260+
fail("Expecting a JSONException");
1261+
} catch (JSONException e) {
1262+
assertTrue("Wrong throwable thrown: not expecting message <" + e.getMessage() + ">",
1263+
e.getMessage().startsWith("Maximum nesting depth of " + maxNestingDepth));
1264+
}
1265+
}
1266+
1267+
@Test
1268+
public void testMaxNestingDepthIsRespectedWithValidXML() {
1269+
final String perfectlyFineXML = "<Test>\n" +
1270+
" <employee>\n" +
1271+
" <name>sonoo</name>\n" +
1272+
" <salary>56000</salary>\n" +
1273+
" <married>true</married>\n" +
1274+
" </employee>\n" +
1275+
"</Test>\n";
1276+
1277+
final int maxNestingDepth = 1;
1278+
1279+
try {
1280+
XML.toJSONObject(perfectlyFineXML, XMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
1281+
1282+
fail("Expecting a JSONException");
1283+
} catch (JSONException e) {
1284+
assertTrue("Wrong throwable thrown: not expecting message <" + e.getMessage() + ">",
1285+
e.getMessage().startsWith("Maximum nesting depth of " + maxNestingDepth));
1286+
}
1287+
}
1288+
1289+
@Test
1290+
public void testMaxNestingDepthWithValidFittingXML() {
1291+
final String perfectlyFineXML = "<Test>\n" +
1292+
" <employee>\n" +
1293+
" <name>sonoo</name>\n" +
1294+
" <salary>56000</salary>\n" +
1295+
" <married>true</married>\n" +
1296+
" </employee>\n" +
1297+
"</Test>\n";
1298+
1299+
final int maxNestingDepth = 3;
1300+
1301+
try {
1302+
XML.toJSONObject(perfectlyFineXML, XMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
1303+
} catch (JSONException e) {
1304+
e.printStackTrace();
1305+
fail("XML document should be parsed as its maximum depth fits the maxNestingDepth " +
1306+
"parameter of the XMLParserConfiguration used");
1307+
}
1308+
}
12501309
}
12511310

12521311

0 commit comments

Comments
 (0)