fix: limit the nesting depth in JSONML

Limit the XML nesting depth for CVE-2022-45688 when using the JsonML transform.
This commit is contained in:
Tamas Perger
2023-02-10 01:46:44 +00:00
parent 2391d248cc
commit a6e412bded
3 changed files with 322 additions and 59 deletions

View File

@@ -11,19 +11,19 @@ import org.junit.Test;
/**
* Tests for org.json.JSONML.java
*
*
* Certain inputs are expected to result in exceptions. These tests are
* executed first. JSONML provides an API to:
* Convert an XML string into a JSONArray or a JSONObject.
* Convert an XML string into a JSONArray or a JSONObject.
* Convert a JSONArray or JSONObject into an XML string.
* Both fromstring and tostring operations operations should be symmetrical
* within the limits of JSONML.
* within the limits of JSONML.
* It should be possible to perform the following operations, which should
* result in the original string being recovered, within the limits of the
* underlying classes:
* Convert a string -> JSONArray -> string -> JSONObject -> string
* Convert a string -> JSONObject -> string -> JSONArray -> string
*
*
*/
public class JSONMLTest {
@@ -56,7 +56,7 @@ public class JSONMLTest {
/**
* Attempts to call JSONML.toString() with a null JSONArray.
* Expects a NullPointerException.
* Expects a NullPointerException.
*/
@Test(expected=NullPointerException.class)
public void nullJSONXMLException() {
@@ -69,7 +69,7 @@ public class JSONMLTest {
/**
* Attempts to call JSONML.toString() with a null JSONArray.
* Expects a JSONException.
* Expects a JSONException.
*/
@Test
public void emptyJSONXMLException() {
@@ -125,7 +125,7 @@ public class JSONMLTest {
"[\"addresses\","+
"{\"xsi:noNamespaceSchemaLocation\":\"test.xsd\","+
"\"xmlns:xsi\":\"http://www.w3.org/2001/XMLSchema-instance\"},"+
// this array has no name
// this array has no name
"["+
"[\"name\"],"+
"[\"nocontent\"],"+
@@ -180,7 +180,7 @@ public class JSONMLTest {
}
/**
* Attempts to transform a malformed XML document
* Attempts to transform a malformed XML document
* (element tag has a frontslash) to a JSONArray.\
* Expects a JSONException
*/
@@ -191,7 +191,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because the 'name' element
* contains an invalid frontslash.
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -216,7 +216,7 @@ public class JSONMLTest {
*/
@Test
public void invalidBangInTagException() {
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -246,7 +246,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element
* starts with '!' and has no closing tag
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -276,7 +276,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element
* has no closing '>'.
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -306,7 +306,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element
* has no name after the closing tag '</'.
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -336,7 +336,7 @@ public class JSONMLTest {
* In this case, the XML is invalid because an element
* has '>' after the closing tag '</' and name.
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation=\"test.xsd\">\n"+
@@ -364,9 +364,9 @@ public class JSONMLTest {
/**
* xmlStr contains XML text which is transformed into a JSONArray.
* In this case, the XML is invalid because an element
* does not have a complete CDATA string.
* does not have a complete CDATA string.
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
" xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -388,7 +388,7 @@ public class JSONMLTest {
/**
* Convert an XML document into a JSONArray, then use JSONML.toString()
* to convert it into a string. This string is then converted back into
* a JSONArray. Both JSONArrays are compared against a control to
* a JSONArray. Both JSONArrays are compared against a control to
* confirm the contents.
*/
@Test
@@ -405,7 +405,7 @@ public class JSONMLTest {
* which is used to create a final JSONArray, which is also compared
* against the expected JSONArray.
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
"xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -414,7 +414,7 @@ public class JSONMLTest {
"<nocontent/>>\n"+
"</address>\n"+
"</addresses>";
String expectedStr =
String expectedStr =
"[\"addresses\","+
"{\"xsi:noNamespaceSchemaLocation\":\"test.xsd\","+
"\"xmlns:xsi\":\"http://www.w3.org/2001/XMLSchema-instance\"},"+
@@ -434,12 +434,12 @@ public class JSONMLTest {
}
/**
* Convert an XML document into a JSONObject. Use JSONML.toString() to
* Convert an XML document into a JSONObject. Use JSONML.toString() to
* convert it back into a string, and then re-convert it into a JSONObject.
* Both JSONObjects are compared against a control JSONObject to confirm
* the contents.
* <p>
* Next convert the XML document into a JSONArray. Use JSONML.toString() to
* Next convert the XML document into a JSONArray. Use JSONML.toString() to
* convert it back into a string, and then re-convert it into a JSONArray.
* Both JSONArrays are compared against a control JSONArray to confirm
* the contents.
@@ -452,23 +452,23 @@ public class JSONMLTest {
/**
* xmlStr contains XML text which is transformed into a JSONObject,
* restored to XML, transformed into a JSONArray, and then restored
* to XML again. Both JSONObject and JSONArray should contain the same
* to XML again. Both JSONObject and JSONArray should contain the same
* information and should produce the same XML, allowing for non-ordered
* attributes.
*
*
* Transformation to JSONObject:
* The elementName is stored as a string where key="tagName"
* Attributes are simply stored as key/value pairs
* If the element has either content or child elements, they are stored
* in a jsonArray with key="childNodes".
*
*
* Transformation to JSONArray:
* 1st entry = elementname
* 2nd entry = attributes object (if present)
* 3rd entry = content (if present)
* 4th entry = child element JSONArrays (if present)
*/
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<addresses xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
"xsi:noNamespaceSchemaLocation='test.xsd'>\n"+
@@ -585,7 +585,7 @@ public class JSONMLTest {
"\"tagName\":\"addresses\""+
"}";
String expectedJSONArrayStr =
String expectedJSONArrayStr =
"["+
"\"addresses\","+
"{"+
@@ -645,12 +645,12 @@ public class JSONMLTest {
JSONObject finalJsonObject = JSONML.toJSONObject(jsonObjectXmlToStr);
Util.compareActualVsExpectedJsonObjects(finalJsonObject, expectedJsonObject);
// create a JSON array from the original string and make sure it
// create a JSON array from the original string and make sure it
// looks as expected
JSONArray jsonArray = JSONML.toJSONArray(xmlStr);
JSONArray expectedJsonArray = new JSONArray(expectedJSONArrayStr);
Util.compareActualVsExpectedJsonArrays(jsonArray,expectedJsonArray);
// restore the XML, then make another JSONArray and make sure it
// looks as expected
String jsonArrayXmlToStr = JSONML.toString(jsonArray);
@@ -668,14 +668,14 @@ public class JSONMLTest {
* Convert an XML document which contains embedded comments into
* a JSONArray. Use JSONML.toString() to turn it into a string, then
* reconvert it into a JSONArray. Compare both JSONArrays to a control
* JSONArray to confirm the contents.
* JSONArray to confirm the contents.
* <p>
* This test shows how XML comments are handled.
*/
@Test
public void commentsInXML() {
String xmlStr =
String xmlStr =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<!-- this is a comment -->\n"+
"<addresses>\n"+
@@ -734,7 +734,7 @@ public class JSONMLTest {
final String expectedJsonString = "[\"root\",[\"id\",\"01\"],[\"id\",\"1\"],[\"id\",\"00\"],[\"id\",\"0\"],[\"item\",{\"id\":\"01\"}],[\"title\",\"True\"]]";
final JSONArray json = JSONML.toJSONArray(originalXml,true);
assertEquals(expectedJsonString, json.toString());
final String reverseXml = JSONML.toString(json);
assertEquals(originalXml, reverseXml);
}
@@ -749,7 +749,7 @@ public class JSONMLTest {
final String revertedXml = JSONML.toString(jsonArray);
assertEquals(revertedXml, originalXml);
}
/**
* JSON string cannot be reverted to original xml. See test result in
* comment below.
@@ -770,7 +770,7 @@ public class JSONMLTest {
// 1. Our XML parser does not handle generic HTML entities, only valid XML entities. Hence &nbsp;
// or other HTML specific entities would fail on reversability
// 2. Our JSON implementation for storing the XML attributes uses the standard unordered map.
// This means that <tag attr1="v1" attr2="v2" /> can not be reversed reliably.
// This means that <tag attr1="v1" attr2="v2" /> can not be reversed reliably.
//
// /**
// * Test texts taken from jsonml.org. Currently our implementation FAILS this conversion but shouldn't.
@@ -783,13 +783,13 @@ public class JSONMLTest {
// final String expectedJsonString = "[\"table\",{\"class\" : \"MyTable\",\"style\" : \"background-color:yellow\"},[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#550758\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:red\"},\"Example text here\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#993101\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:green\"},\"127624015\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#E33D87\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:blue\"},\"\u00A0\",[\"span\",{ \"style\" : \"background-color:maroon\" },\"\u00A9\"],\"\u00A0\"]]]";
// final JSONArray json = JSONML.toJSONArray(originalXml,true);
// final String actualJsonString = json.toString();
//
//
// final String reverseXml = JSONML.toString(json);
// assertNotEquals(originalXml, reverseXml);
//
// assertNotEquals(expectedJsonString, actualJsonString);
// }
//
//
// /**
// * Test texts taken from jsonml.org but modified to have XML entities only.
// */
@@ -799,15 +799,15 @@ public class JSONMLTest {
// final String expectedJsonString = "[\"table\",{\"class\" : \"MyTable\",\"style\" : \"background-color:yellow\"},[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#550758\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:red\"},\"Example text here\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#993101\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:green\"},\"127624015\"]],[\"tr\",[\"td\",{\"class\" : \"MyTD\",\"style\" : \"border:1px solid black\"},\"#E33D87\"],[\"td\",{\"class\" : \"MyTD\",\"style\" : \"background-color:blue\"},\"&\",[\"span\",{ \"style\" : \"background-color:maroon\" },\">\"],\"<\"]]]";
// final JSONArray jsonML = JSONML.toJSONArray(originalXml,true);
// final String actualJsonString = jsonML.toString();
//
//
// final String reverseXml = JSONML.toString(jsonML);
// // currently not equal because the hashing of the attribute objects makes the attribute
// // order not happen the same way twice
// // order not happen the same way twice
// assertEquals(originalXml, reverseXml);
//
// assertEquals(expectedJsonString, actualJsonString);
// }
@Test (timeout = 6000)
public void testIssue484InfinteLoop1() {
try {
@@ -819,11 +819,11 @@ public class JSONMLTest {
ex.getMessage());
}
}
@Test (timeout = 6000)
public void testIssue484InfinteLoop2() {
try {
String input = "??*\n" +
String input = "??*\n" +
"??|?CglR??`??>?w??PIlr??D?$?-?o??O?*??{OD?Y??`2a????NM?bq?:O?>S$ ?J?B.gUK?m\b??zE???!v]???????c??????h???s???g???`?qbi??:Zl?)?}1^??k?0??:$V?$?Ovs(}J??????2;gQ????Tg?K?`?h%c?hmGA?<!C*?9?~?t?)??,zA???S}?Q??.q?j????]";
JSONML.toJSONObject(input);
fail("Exception expected for invalid JSON.");
@@ -833,4 +833,64 @@ public class JSONMLTest {
ex.getMessage());
}
}
@Test
public void testMaxNestingDepthOf42IsRespected() {
final String wayTooLongMalformedXML = new String(new char[6000]).replace("\0", "<a>");
final int maxNestingDepth = 42;
try {
JSONML.toJSONObject(wayTooLongMalformedXML, XMLtoJSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
fail("Expecting a JSONException");
} catch (JSONException e) {
assertTrue("Wrong throwable thrown: not expecting message <" + e.getMessage() + ">",
e.getMessage().startsWith("Maximum nesting depth of " + maxNestingDepth));
}
}
@Test
public void testMaxNestingDepthIsRespectedWithValidXML() {
final String perfectlyFineXML = "<Test>\n" +
" <employee>\n" +
" <name>sonoo</name>\n" +
" <salary>56000</salary>\n" +
" <married>true</married>\n" +
" </employee>\n" +
"</Test>\n";
final int maxNestingDepth = 1;
try {
JSONML.toJSONObject(perfectlyFineXML, XMLtoJSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
fail("Expecting a JSONException");
} catch (JSONException e) {
assertTrue("Wrong throwable thrown: not expecting message <" + e.getMessage() + ">",
e.getMessage().startsWith("Maximum nesting depth of " + maxNestingDepth));
}
}
@Test
public void testMaxNestingDepthWithValidFittingXML() {
final String perfectlyFineXML = "<Test>\n" +
" <employee>\n" +
" <name>sonoo</name>\n" +
" <salary>56000</salary>\n" +
" <married>true</married>\n" +
" </employee>\n" +
"</Test>\n";
final int maxNestingDepth = 3;
try {
JSONML.toJSONObject(perfectlyFineXML, XMLtoJSONMLParserConfiguration.ORIGINAL.withMaxNestingDepth(maxNestingDepth));
} catch (JSONException e) {
e.printStackTrace();
fail("XML document should be parsed as its maximum depth fits the maxNestingDepth " +
"parameter of the XMLtoJSONMLParserConfiguration used");
}
}
}