Skip to content

Commit a6e412b

Browse files
fix: limit the nesting depth in JSONML
Limit the XML nesting depth for CVE-2022-45688 when using the JsonML transform.
1 parent 2391d24 commit a6e412b

File tree

3 files changed

+322
-59
lines changed

3 files changed

+322
-59
lines changed

src/main/java/org/json/JSONML.java

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,32 @@ private static Object parse(
2727
XMLTokener x,
2828
boolean arrayForm,
2929
JSONArray ja,
30-
boolean keepStrings
30+
boolean keepStrings,
31+
int currentNestingDepth
32+
) throws JSONException {
33+
return parse(x,arrayForm, ja,
34+
keepStrings ? XMLtoJSONMLParserConfiguration.KEEP_STRINGS : XMLtoJSONMLParserConfiguration.ORIGINAL,
35+
currentNestingDepth);
36+
}
37+
38+
/**
39+
* Parse XML values and store them in a JSONArray.
40+
* @param x The XMLTokener containing the source string.
41+
* @param arrayForm true if array form, false if object form.
42+
* @param ja The JSONArray that is containing the current tag or null
43+
* if we are at the outermost level.
44+
* @param config The XML parser configuration:
45+
* XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour;
46+
* XMLtoJSONMLParserConfiguration.KEEP_STRINGS means Don't type-convert text nodes and attribute values.
47+
* @return A JSONArray if the value is the outermost tag, otherwise null.
48+
* @throws JSONException if a parsing error occurs
49+
*/
50+
private static Object parse(
51+
XMLTokener x,
52+
boolean arrayForm,
53+
JSONArray ja,
54+
XMLtoJSONMLParserConfiguration config,
55+
int currentNestingDepth
3156
) throws JSONException {
3257
String attribute;
3358
char c;
@@ -152,7 +177,7 @@ private static Object parse(
152177
if (!(token instanceof String)) {
153178
throw x.syntaxError("Missing value");
154179
}
155-
newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token));
180+
newjo.accumulate(attribute, config.isKeepStrings() ? ((String)token) :XML.stringToValue((String)token));
156181
token = null;
157182
} else {
158183
newjo.accumulate(attribute, "");
@@ -181,7 +206,12 @@ private static Object parse(
181206
if (token != XML.GT) {
182207
throw x.syntaxError("Misshaped tag");
183208
}
184-
closeTag = (String)parse(x, arrayForm, newja, keepStrings);
209+
210+
if (currentNestingDepth == config.getMaxNestingDepth()) {
211+
throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached");
212+
}
213+
214+
closeTag = (String)parse(x, arrayForm, newja, config, currentNestingDepth + 1);
185215
if (closeTag != null) {
186216
if (!closeTag.equals(tagName)) {
187217
throw x.syntaxError("Mismatched '" + tagName +
@@ -203,7 +233,7 @@ private static Object parse(
203233
} else {
204234
if (ja != null) {
205235
ja.put(token instanceof String
206-
? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token)
236+
? (config.isKeepStrings() ? XML.unescape((String)token) : XML.stringToValue((String)token))
207237
: token);
208238
}
209239
}
@@ -224,7 +254,7 @@ private static Object parse(
224254
* @throws JSONException Thrown on error converting to a JSONArray
225255
*/
226256
public static JSONArray toJSONArray(String string) throws JSONException {
227-
return (JSONArray)parse(new XMLTokener(string), true, null, false);
257+
return (JSONArray)parse(new XMLTokener(string), true, null, XMLtoJSONMLParserConfiguration.ORIGINAL, 0);
228258
}
229259

230260

@@ -235,8 +265,8 @@ public static JSONArray toJSONArray(String string) throws JSONException {
235265
* attributes, then the second element will be JSONObject containing the
236266
* name/value pairs. If the tag contains children, then strings and
237267
* JSONArrays will represent the child tags.
238-
* As opposed to toJSONArray this method does not attempt to convert
239-
* any text node or attribute value to any type
268+
* As opposed to toJSONArray this method does not attempt to convert
269+
* any text node or attribute value to any type
240270
* but just leaves it as a string.
241271
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
242272
* @param string The source string.
@@ -246,7 +276,7 @@ public static JSONArray toJSONArray(String string) throws JSONException {
246276
* @throws JSONException Thrown on error converting to a JSONArray
247277
*/
248278
public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException {
249-
return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings);
279+
return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings, 0);
250280
}
251281

252282

@@ -257,8 +287,8 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J
257287
* attributes, then the second element will be JSONObject containing the
258288
* name/value pairs. If the tag contains children, then strings and
259289
* JSONArrays will represent the child content and tags.
260-
* As opposed to toJSONArray this method does not attempt to convert
261-
* any text node or attribute value to any type
290+
* As opposed to toJSONArray this method does not attempt to convert
291+
* any text node or attribute value to any type
262292
* but just leaves it as a string.
263293
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
264294
* @param x An XMLTokener.
@@ -268,7 +298,7 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J
268298
* @throws JSONException Thrown on error converting to a JSONArray
269299
*/
270300
public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException {
271-
return (JSONArray)parse(x, true, null, keepStrings);
301+
return (JSONArray)parse(x, true, null, keepStrings, 0);
272302
}
273303

274304

@@ -285,7 +315,7 @@ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JS
285315
* @throws JSONException Thrown on error converting to a JSONArray
286316
*/
287317
public static JSONArray toJSONArray(XMLTokener x) throws JSONException {
288-
return (JSONArray)parse(x, true, null, false);
318+
return (JSONArray)parse(x, true, null, false, 0);
289319
}
290320

291321

@@ -303,10 +333,10 @@ public static JSONArray toJSONArray(XMLTokener x) throws JSONException {
303333
* @throws JSONException Thrown on error converting to a JSONObject
304334
*/
305335
public static JSONObject toJSONObject(String string) throws JSONException {
306-
return (JSONObject)parse(new XMLTokener(string), false, null, false);
336+
return (JSONObject)parse(new XMLTokener(string), false, null, false, 0);
307337
}
308-
309-
338+
339+
310340
/**
311341
* Convert a well-formed (but not necessarily valid) XML string into a
312342
* JSONObject using the JsonML transform. Each XML tag is represented as
@@ -323,10 +353,32 @@ public static JSONObject toJSONObject(String string) throws JSONException {
323353
* @throws JSONException Thrown on error converting to a JSONObject
324354
*/
325355
public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException {
326-
return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings);
356+
return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings, 0);
327357
}
328358

329-
359+
360+
/**
361+
* Convert a well-formed (but not necessarily valid) XML string into a
362+
* JSONObject using the JsonML transform. Each XML tag is represented as
363+
* a JSONObject with a "tagName" property. If the tag has attributes, then
364+
* the attributes will be in the JSONObject as properties. If the tag
365+
* contains children, the object will have a "childNodes" property which
366+
* will be an array of strings and JsonML JSONObjects.
367+
368+
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
369+
* @param string The XML source text.
370+
* @param config The XML parser configuration:
371+
* XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour;
372+
* XMLtoJSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
373+
* or numeric values and will instead be left as strings
374+
* @return A JSONObject containing the structured data from the XML string.
375+
* @throws JSONException Thrown on error converting to a JSONObject
376+
*/
377+
public static JSONObject toJSONObject(String string, XMLtoJSONMLParserConfiguration config) throws JSONException {
378+
return (JSONObject)parse(new XMLTokener(string), false, null, config, 0);
379+
}
380+
381+
330382
/**
331383
* Convert a well-formed (but not necessarily valid) XML string into a
332384
* JSONObject using the JsonML transform. Each XML tag is represented as
@@ -341,7 +393,7 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws
341393
* @throws JSONException Thrown on error converting to a JSONObject
342394
*/
343395
public static JSONObject toJSONObject(XMLTokener x) throws JSONException {
344-
return (JSONObject)parse(x, false, null, false);
396+
return (JSONObject)parse(x, false, null, false, 0);
345397
}
346398

347399

@@ -361,7 +413,29 @@ public static JSONObject toJSONObject(XMLTokener x) throws JSONException {
361413
* @throws JSONException Thrown on error converting to a JSONObject
362414
*/
363415
public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException {
364-
return (JSONObject)parse(x, false, null, keepStrings);
416+
return (JSONObject)parse(x, false, null, keepStrings, 0);
417+
}
418+
419+
420+
/**
421+
* Convert a well-formed (but not necessarily valid) XML string into a
422+
* JSONObject using the JsonML transform. Each XML tag is represented as
423+
* a JSONObject with a "tagName" property. If the tag has attributes, then
424+
* the attributes will be in the JSONObject as properties. If the tag
425+
* contains children, the object will have a "childNodes" property which
426+
* will be an array of strings and JsonML JSONObjects.
427+
428+
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
429+
* @param x An XMLTokener of the XML source text.
430+
* @param config The XML parser configuration:
431+
* XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour;
432+
* XMLtoJSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
433+
* or numeric values and will instead be left as strings
434+
* @return A JSONObject containing the structured data from the XML string.
435+
* @throws JSONException Thrown on error converting to a JSONObject
436+
*/
437+
public static JSONObject toJSONObject(XMLTokener x, XMLtoJSONMLParserConfiguration config) throws JSONException {
438+
return (JSONObject)parse(x, false, null, config, 0);
365439
}
366440

367441

@@ -442,6 +516,7 @@ public static String toString(JSONArray ja) throws JSONException {
442516
return sb.toString();
443517
}
444518

519+
445520
/**
446521
* Reverse the JSONML transformation, making an XML text from a JSONObject.
447522
* The JSONObject must contain a "tagName" property. If it has children,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package org.json;
2+
/*
3+
Public Domain.
4+
*/
5+
6+
/**
7+
* Configuration object for the XML to JSONML parser. The configuration is immutable.
8+
*/
9+
@SuppressWarnings({""})
10+
public class XMLtoJSONMLParserConfiguration {
11+
/**
12+
* Used to indicate there's no defined limit to the maximum nesting depth when parsing a XML
13+
* document to JSONML.
14+
*/
15+
public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1;
16+
17+
/**
18+
* The default maximum nesting depth when parsing a XML document to JSONML.
19+
*/
20+
public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512;
21+
22+
/** Original Configuration of the XML to JSONML Parser. */
23+
public static final XMLtoJSONMLParserConfiguration ORIGINAL
24+
= new XMLtoJSONMLParserConfiguration();
25+
/** Original configuration of the XML to JSONML Parser except that values are kept as strings. */
26+
public static final XMLtoJSONMLParserConfiguration KEEP_STRINGS
27+
= new XMLtoJSONMLParserConfiguration().withKeepStrings(true);
28+
29+
/**
30+
* When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if
31+
* they should try to be guessed into JSON values (numeric, boolean, string)
32+
*/
33+
private boolean keepStrings;
34+
35+
/**
36+
* The maximum nesting depth when parsing a XML document to JSONML.
37+
*/
38+
private int maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH;
39+
40+
/**
41+
* Default parser configuration. Does not keep strings (tries to implicitly convert values).
42+
*/
43+
public XMLtoJSONMLParserConfiguration() {
44+
this.keepStrings = false;
45+
}
46+
47+
/**
48+
* Configure the parser string processing and use the default CDATA Tag Name as "content".
49+
* @param keepStrings <code>true</code> to parse all values as string.
50+
* <code>false</code> to try and convert XML string values into a JSON value.
51+
* @param maxNestingDepth <code>int</code> to limit the nesting depth
52+
*/
53+
public XMLtoJSONMLParserConfiguration(final boolean keepStrings, final int maxNestingDepth) {
54+
this.keepStrings = keepStrings;
55+
this.maxNestingDepth = maxNestingDepth;
56+
}
57+
58+
/**
59+
* Provides a new instance of the same configuration.
60+
*/
61+
@Override
62+
protected XMLtoJSONMLParserConfiguration clone() {
63+
// future modifications to this method should always ensure a "deep"
64+
// clone in the case of collections. i.e. if a Map is added as a configuration
65+
// item, a new map instance should be created and if possible each value in the
66+
// map should be cloned as well. If the values of the map are known to also
67+
// be immutable, then a shallow clone of the map is acceptable.
68+
return new XMLtoJSONMLParserConfiguration(
69+
this.keepStrings,
70+
this.maxNestingDepth
71+
);
72+
}
73+
74+
/**
75+
* When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if
76+
* they should try to be guessed into JSON values (numeric, boolean, string)
77+
*
78+
* @return The <code>keepStrings</code> configuration value.
79+
*/
80+
public boolean isKeepStrings() {
81+
return this.keepStrings;
82+
}
83+
84+
/**
85+
* When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if
86+
* they should try to be guessed into JSON values (numeric, boolean, string)
87+
*
88+
* @param newVal
89+
* new value to use for the <code>keepStrings</code> configuration option.
90+
*
91+
* @return The existing configuration will not be modified. A new configuration is returned.
92+
*/
93+
public XMLtoJSONMLParserConfiguration withKeepStrings(final boolean newVal) {
94+
XMLtoJSONMLParserConfiguration newConfig = this.clone();
95+
newConfig.keepStrings = newVal;
96+
return newConfig;
97+
}
98+
99+
/**
100+
* The maximum nesting depth that the parser will descend before throwing an exception
101+
* when parsing the XML into JSONML.
102+
* @return the maximum nesting depth set for this configuration
103+
*/
104+
public int getMaxNestingDepth() {
105+
return maxNestingDepth;
106+
}
107+
108+
/**
109+
* Defines the maximum nesting depth that the parser will descend before throwing an exception
110+
* when parsing the XML into JSONML. The default max nesting depth is 512, which means the parser
111+
* will throw a JsonException if the maximum depth is reached.
112+
* Using any negative value as a parameter is equivalent to setting no limit to the nesting depth,
113+
* which means the parses will go as deep as the maximum call stack size allows.
114+
* @param maxNestingDepth the maximum nesting depth allowed to the XML parser
115+
* @return The existing configuration will not be modified. A new configuration is returned.
116+
*/
117+
public XMLtoJSONMLParserConfiguration withMaxNestingDepth(int maxNestingDepth) {
118+
XMLtoJSONMLParserConfiguration newConfig = this.clone();
119+
120+
if (maxNestingDepth > UNDEFINED_MAXIMUM_NESTING_DEPTH) {
121+
newConfig.maxNestingDepth = maxNestingDepth;
122+
} else {
123+
newConfig.maxNestingDepth = UNDEFINED_MAXIMUM_NESTING_DEPTH;
124+
}
125+
126+
return newConfig;
127+
}
128+
}

0 commit comments

Comments
 (0)