Skip to content

Commit 1275f68

Browse files
authored
Merge pull request stleary#723 from TamasPergerDWP/master
JSONML should be protected from stack overflow exceptions caused by recursion, resolving stleary#722
2 parents 401495a + 9234eab commit 1275f68

File tree

5 files changed

+475
-91
lines changed

5 files changed

+475
-91
lines changed

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

+143-19
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 ? JSONMLParserConfiguration.KEEP_STRINGS : JSONMLParserConfiguration.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 parser configuration:
45+
* JSONMLParserConfiguration.ORIGINAL is the default behaviour;
46+
* JSONMLParserConfiguration.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+
JSONMLParserConfiguration 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, JSONMLParserConfiguration.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,56 @@ 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);
280+
}
281+
282+
283+
284+
/**
285+
* Convert a well-formed (but not necessarily valid) XML string into a
286+
* JSONArray using the JsonML transform. Each XML tag is represented as
287+
* a JSONArray in which the first element is the tag name. If the tag has
288+
* attributes, then the second element will be JSONObject containing the
289+
* name/value pairs. If the tag contains children, then strings and
290+
* JSONArrays will represent the child tags.
291+
* As opposed to toJSONArray this method does not attempt to convert
292+
* any text node or attribute value to any type
293+
* but just leaves it as a string.
294+
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
295+
* @param string The source string.
296+
* @param config The parser configuration:
297+
* JSONMLParserConfiguration.ORIGINAL is the default behaviour;
298+
* JSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
299+
* or numeric values and will instead be left as strings
300+
* @return A JSONArray containing the structured data from the XML string.
301+
* @throws JSONException Thrown on error converting to a JSONArray
302+
*/
303+
public static JSONArray toJSONArray(String string, JSONMLParserConfiguration config) throws JSONException {
304+
return (JSONArray)parse(new XMLTokener(string), true, null, config, 0);
305+
}
306+
307+
308+
/**
309+
* Convert a well-formed (but not necessarily valid) XML string into a
310+
* JSONArray using the JsonML transform. Each XML tag is represented as
311+
* a JSONArray in which the first element is the tag name. If the tag has
312+
* attributes, then the second element will be JSONObject containing the
313+
* name/value pairs. If the tag contains children, then strings and
314+
* JSONArrays will represent the child content and tags.
315+
* As opposed to toJSONArray this method does not attempt to convert
316+
* any text node or attribute value to any type
317+
* but just leaves it as a string.
318+
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
319+
* @param x An XMLTokener.
320+
* @param config The parser configuration:
321+
* JSONMLParserConfiguration.ORIGINAL is the default behaviour;
322+
* JSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
323+
* or numeric values and will instead be left as strings
324+
* @return A JSONArray containing the structured data from the XML string.
325+
* @throws JSONException Thrown on error converting to a JSONArray
326+
*/
327+
public static JSONArray toJSONArray(XMLTokener x, JSONMLParserConfiguration config) throws JSONException {
328+
return (JSONArray)parse(x, true, null, config, 0);
250329
}
251330

252331

@@ -257,8 +336,8 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J
257336
* attributes, then the second element will be JSONObject containing the
258337
* name/value pairs. If the tag contains children, then strings and
259338
* 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
339+
* As opposed to toJSONArray this method does not attempt to convert
340+
* any text node or attribute value to any type
262341
* but just leaves it as a string.
263342
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
264343
* @param x An XMLTokener.
@@ -268,7 +347,7 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J
268347
* @throws JSONException Thrown on error converting to a JSONArray
269348
*/
270349
public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException {
271-
return (JSONArray)parse(x, true, null, keepStrings);
350+
return (JSONArray)parse(x, true, null, keepStrings, 0);
272351
}
273352

274353

@@ -285,7 +364,7 @@ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JS
285364
* @throws JSONException Thrown on error converting to a JSONArray
286365
*/
287366
public static JSONArray toJSONArray(XMLTokener x) throws JSONException {
288-
return (JSONArray)parse(x, true, null, false);
367+
return (JSONArray)parse(x, true, null, false, 0);
289368
}
290369

291370

@@ -303,10 +382,10 @@ public static JSONArray toJSONArray(XMLTokener x) throws JSONException {
303382
* @throws JSONException Thrown on error converting to a JSONObject
304383
*/
305384
public static JSONObject toJSONObject(String string) throws JSONException {
306-
return (JSONObject)parse(new XMLTokener(string), false, null, false);
385+
return (JSONObject)parse(new XMLTokener(string), false, null, false, 0);
307386
}
308-
309-
387+
388+
310389
/**
311390
* Convert a well-formed (but not necessarily valid) XML string into a
312391
* JSONObject using the JsonML transform. Each XML tag is represented as
@@ -323,10 +402,32 @@ public static JSONObject toJSONObject(String string) throws JSONException {
323402
* @throws JSONException Thrown on error converting to a JSONObject
324403
*/
325404
public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException {
326-
return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings);
405+
return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings, 0);
406+
}
407+
408+
409+
/**
410+
* Convert a well-formed (but not necessarily valid) XML string into a
411+
* JSONObject using the JsonML transform. Each XML tag is represented as
412+
* a JSONObject with a "tagName" property. If the tag has attributes, then
413+
* the attributes will be in the JSONObject as properties. If the tag
414+
* contains children, the object will have a "childNodes" property which
415+
* will be an array of strings and JsonML JSONObjects.
416+
417+
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
418+
* @param string The XML source text.
419+
* @param config The parser configuration:
420+
* JSONMLParserConfiguration.ORIGINAL is the default behaviour;
421+
* JSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
422+
* or numeric values and will instead be left as strings
423+
* @return A JSONObject containing the structured data from the XML string.
424+
* @throws JSONException Thrown on error converting to a JSONObject
425+
*/
426+
public static JSONObject toJSONObject(String string, JSONMLParserConfiguration config) throws JSONException {
427+
return (JSONObject)parse(new XMLTokener(string), false, null, config, 0);
327428
}
328429

329-
430+
330431
/**
331432
* Convert a well-formed (but not necessarily valid) XML string into a
332433
* JSONObject using the JsonML transform. Each XML tag is represented as
@@ -341,7 +442,7 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws
341442
* @throws JSONException Thrown on error converting to a JSONObject
342443
*/
343444
public static JSONObject toJSONObject(XMLTokener x) throws JSONException {
344-
return (JSONObject)parse(x, false, null, false);
445+
return (JSONObject)parse(x, false, null, false, 0);
345446
}
346447

347448

@@ -361,7 +462,29 @@ public static JSONObject toJSONObject(XMLTokener x) throws JSONException {
361462
* @throws JSONException Thrown on error converting to a JSONObject
362463
*/
363464
public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException {
364-
return (JSONObject)parse(x, false, null, keepStrings);
465+
return (JSONObject)parse(x, false, null, keepStrings, 0);
466+
}
467+
468+
469+
/**
470+
* Convert a well-formed (but not necessarily valid) XML string into a
471+
* JSONObject using the JsonML transform. Each XML tag is represented as
472+
* a JSONObject with a "tagName" property. If the tag has attributes, then
473+
* the attributes will be in the JSONObject as properties. If the tag
474+
* contains children, the object will have a "childNodes" property which
475+
* will be an array of strings and JsonML JSONObjects.
476+
477+
* Comments, prologs, DTDs, and <pre>{@code &lt;[ [ ]]>}</pre> are ignored.
478+
* @param x An XMLTokener of the XML source text.
479+
* @param config The parser configuration:
480+
* JSONMLParserConfiguration.ORIGINAL is the default behaviour;
481+
* JSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean
482+
* or numeric values and will instead be left as strings
483+
* @return A JSONObject containing the structured data from the XML string.
484+
* @throws JSONException Thrown on error converting to a JSONObject
485+
*/
486+
public static JSONObject toJSONObject(XMLTokener x, JSONMLParserConfiguration config) throws JSONException {
487+
return (JSONObject)parse(x, false, null, config, 0);
365488
}
366489

367490

@@ -442,6 +565,7 @@ public static String toString(JSONArray ja) throws JSONException {
442565
return sb.toString();
443566
}
444567

568+
445569
/**
446570
* Reverse the JSONML transformation, making an XML text from a JSONObject.
447571
* The JSONObject must contain a "tagName" property. If it has children,
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 JSONMLParserConfiguration {
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 JSONMLParserConfiguration ORIGINAL
24+
= new JSONMLParserConfiguration();
25+
/** Original configuration of the XML to JSONML Parser except that values are kept as strings. */
26+
public static final JSONMLParserConfiguration KEEP_STRINGS
27+
= new JSONMLParserConfiguration().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 JSONMLParserConfiguration() {
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+
private JSONMLParserConfiguration(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 JSONMLParserConfiguration 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 JSONMLParserConfiguration(
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 JSONMLParserConfiguration withKeepStrings(final boolean newVal) {
94+
JSONMLParserConfiguration 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 JSONMLParserConfiguration withMaxNestingDepth(int maxNestingDepth) {
118+
JSONMLParserConfiguration 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)