CVE-2022-45688
Description
Hutool-json v5.8.10 XML.toJSONObject stack overflow via deeply nested XML/JSON strings can cause Denial of Service.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Hutool-json v5.8.10 XML.toJSONObject stack overflow via deeply nested XML/JSON strings can cause Denial of Service.
Vulnerability
CVE-2022-45688 describes a stack overflow vulnerability in the XML.toJSONObject component of hutool-json v5.8.10 [1][4]. The root cause is the lack of depth limiting during recursive parsing of deeply nested XML or JSON strings [1][2].
Exploitation
An attacker can craft a payload consisting of deeply nested, self-referential XML elements (e.g., <a><a>...) or equivalent JSON strings [1][2]. When the vulnerable code parses such input, it recurses deeply until the call stack overflows, triggering a StackOverflowError and causing the Java application to crash [1][2].
Impact
Successful exploitation results in a Denial of Service (DoS) condition: the target application becomes unavailable [1][2][4]. No authentication or special privileges are required—merely sending the malicious payload to a service that parses untrusted XML/JSON with hutool-json can trigger the crash [1][2].
Mitigation
The vulnerability affects hutool-json v5.8.10 [1]. A fix was introduced in the upstream stleary/JSON-java repository by limiting the nesting depth during parsing [3]. Users should upgrade hutool-json to a patched version (e.g., 5.8.11 or later) or apply an equivalent workaround [3]. The issue is publicly disclosed with proof-of-concept code [1][2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
cn.hutool:hutool-jsonMaven | < 5.8.25 | 5.8.25 |
org.json:jsonMaven | < 20230227 | 20230227 |
Affected products
5- hutool/hutool-jsondescription
- osv-coords4 versionspkg:apk/chainguard/hadoop-fips-3.3.6pkg:maven/cn.hutool/hutool-jsonpkg:maven/org.json/jsonpkg:rpm/opensuse/json-java&distro=openSUSE%20Tumbleweed
< 3.3.6-r15+ 3 more
- (no CPE)range: < 3.3.6-r15
- (no CPE)range: < 5.8.25
- (no CPE)range: < 20230227
- (no CPE)range: < 20240303-1.1
Patches
26a2b585de0a3增加ParseConfig,通过增加maxNestingDepth参数避免StackOverflowError问题,修复CVE-2022-45688漏洞
5 files changed · +170 −6
CHANGELOG.md+1 −0 modified@@ -15,6 +15,7 @@ * 【core 】 修复RandomUtil.randomInt,RandomUtil.randomLong边界问题(pr#3450@Github) * 【db 】 修复Druid连接池无法设置部分属性问题(issue#I8STFC@Gitee) * 【core 】 修复金额转换为英文时缺少 trillion 单位问题(pr#3454@Github) +* 【json 】 增加ParseConfig,通过增加maxNestingDepth参数避免StackOverflowError问题,修复CVE-2022-45688漏洞(issue#2748@Github) ------------------------------------------------------------------------------------------------------------- # 5.8.24(2023-12-23)
hutool-json/src/main/java/cn/hutool/json/XML.java+33 −0 modified@@ -3,6 +3,7 @@ import cn.hutool.core.util.CharUtil; import cn.hutool.json.xml.JSONXMLParser; import cn.hutool.json.xml.JSONXMLSerializer; +import cn.hutool.json.xml.ParseConfig; /** * 提供静态方法在XML和JSONObject之间转换 @@ -86,6 +87,22 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws return toJSONObject(new JSONObject(), string, keepStrings); } + /** + * 转换XML为JSONObject + * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 + * Content text may be placed in a "content" member. Comments, prologs, DTDs, and {@code <[ [ ]]>} are ignored. + * All values are converted as strings, for 1, 01, 29.0 will not be coerced to numbers but will instead be the exact value as seen in the XML document. + * + * @param string XML字符串 + * @param parseConfig XML解析选项 + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown if there is an errors while parsing the string + * @since 5.8.25 + */ + public static JSONObject toJSONObject(final String string, final ParseConfig parseConfig) throws JSONException { + return toJSONObject(new JSONObject(), string, parseConfig); + } + /** * 转换XML为JSONObject * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 @@ -102,6 +119,22 @@ public static JSONObject toJSONObject(JSONObject jo, String xmlStr, boolean keep return jo; } + /** + * 转换XML为JSONObject + * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 + * + * @param jo JSONObject + * @param xmlStr XML字符串 + * @param parseConfig XML解析选项 + * @return A JSONObject 解析后的JSON对象,与传入的jo为同一对象 + * @throws JSONException 解析异常 + * @since 5.8.25 + */ + public static JSONObject toJSONObject(final JSONObject jo, final String xmlStr, final ParseConfig parseConfig) throws JSONException { + JSONXMLParser.parseJSONObject(jo, xmlStr, parseConfig); + return jo; + } + /** * 转换JSONObject为XML *
hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java+29 −6 modified@@ -24,9 +24,22 @@ public class JSONXMLParser { * @throws JSONException 解析异常 */ public static void parseJSONObject(JSONObject jo, String xmlStr, boolean keepStrings) throws JSONException { - XMLTokener x = new XMLTokener(xmlStr, jo.getConfig()); + parseJSONObject(jo, xmlStr, ParseConfig.of().setKeepStrings(keepStrings)); + } + + /** + * 转换XML为JSONObject + * 转换过程中一些信息可能会丢失,JSON中无法区分节点和属性,相同的节点将被处理为JSONArray。 + * + * @param xmlStr XML字符串 + * @param jo JSONObject + * @param parseConfig 解析选项 + * @throws JSONException 解析异常 + */ + public static void parseJSONObject(final JSONObject jo, final String xmlStr, final ParseConfig parseConfig) throws JSONException { + final XMLTokener x = new XMLTokener(xmlStr, jo.getConfig()); while (x.more() && x.skipPast("<")) { - parse(x, jo, null, keepStrings); + parse(x, jo, null, parseConfig, 0); } } @@ -36,10 +49,12 @@ public static void parseJSONObject(JSONObject jo, String xmlStr, boolean keepStr * @param x The XMLTokener containing the source string. * @param context The JSONObject that will include the new material. * @param name The tag name. + * @param parseConfig 解析选项 + * @param currentNestingDepth 当前层级 * @return true if the close tag is processed. * @throws JSONException JSON异常 */ - private static boolean parse(XMLTokener x, JSONObject context, String name, boolean keepStrings) throws JSONException { + private static boolean parse(XMLTokener x, JSONObject context, String name, ParseConfig parseConfig, int currentNestingDepth) throws JSONException { char c; int i; JSONObject jsonobject; @@ -112,6 +127,7 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, bool tagName = (String) token; token = null; jsonobject = new JSONObject(); + final boolean keepStrings = parseConfig.isKeepStrings(); for (; ; ) { if (token == null) { token = x.nextToken(); @@ -155,14 +171,21 @@ private static boolean parse(XMLTokener x, JSONObject context, String name, bool return false; } else if (token instanceof String) { string = (String) token; - if (string.length() > 0) { + if (!string.isEmpty()) { jsonobject.accumulate("content", keepStrings ? token : InternalJSONUtil.stringToValue(string)); } } else if (token == XML.LT) { // Nested element - if (parse(x, jsonobject, tagName, keepStrings)) { - if (jsonobject.size() == 0) { + // issue#2748 of CVE-2022-45688 + final int maxNestingDepth = parseConfig.getMaxNestingDepth(); + if (maxNestingDepth > -1 && currentNestingDepth >= maxNestingDepth) { + throw x.syntaxError("Maximum nesting depth of " + maxNestingDepth + " reached"); + } + + // Nested element + if (parse(x, jsonobject, tagName, parseConfig, currentNestingDepth + 1)) { + if (jsonobject.isEmpty()) { context.accumulate(tagName, ""); } else if (jsonobject.size() == 1 && jsonobject.get("content") != null) { context.accumulate(tagName, jsonobject.get("content"));
hutool-json/src/main/java/cn/hutool/json/xml/ParseConfig.java+88 −0 added@@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024. looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * https://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package cn.hutool.json.xml; + +import java.io.Serializable; + +/** + * XML解析为JSON的可选选项<br> + * 参考:https://github.com/stleary/JSON-java/blob/master/src/main/java/org/json/ParserConfiguration.java + * + * @author AylwardJ, Looly + */ +public class ParseConfig implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 默认最大嵌套深度 + */ + public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; + + /** + * 创建ParseConfig + * + * @return ParseConfig + */ + public static ParseConfig of() { + return new ParseConfig(); + } + + /** + * 是否保持值为String类型,如果为{@code false},则尝试转换为对应类型(numeric, boolean, string) + */ + private boolean keepStrings; + /** + * 最大嵌套深度,用于解析时限制解析层级,当大于这个层级时抛出异常,-1表示无限制 + */ + private int maxNestingDepth = -1; + + /** + * 是否保持值为String类型,如果为{@code false},则尝试转换为对应类型(numeric, boolean, string) + * + * @return 是否保持值为String类型 + */ + public boolean isKeepStrings() { + return keepStrings; + } + + /** + * 设置是否保持值为String类型,如果为{@code false},则尝试转换为对应类型(numeric, boolean, string) + * + * @param keepStrings 是否保持值为String类型 + * @return this + */ + public ParseConfig setKeepStrings(final boolean keepStrings) { + this.keepStrings = keepStrings; + return this; + } + + /** + * 获取最大嵌套深度,用于解析时限制解析层级,当大于这个层级时抛出异常,-1表示无限制 + * + * @return 最大嵌套深度 + */ + public int getMaxNestingDepth() { + return maxNestingDepth; + } + + /** + * 设置最大嵌套深度,用于解析时限制解析层级,当大于这个层级时抛出异常,-1表示无限制 + * + * @param maxNestingDepth 最大嵌套深度 + * @return this + */ + public ParseConfig setMaxNestingDepth(final int maxNestingDepth) { + this.maxNestingDepth = maxNestingDepth; + return this; + } +}
hutool-json/src/test/java/cn/hutool/json/xml/Issue2748Test.java+19 −0 added@@ -0,0 +1,19 @@ +package cn.hutool.json.xml; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.XML; +import org.junit.Assert; +import org.junit.Test; + +public class Issue2748Test { + + @Test + public void toJSONObjectTest() { + final String s = StrUtil.repeat("<a>", 600); + + Assert.assertThrows(JSONException.class, () -> { + XML.toJSONObject(s, ParseConfig.of().setMaxNestingDepth(512)); + }); + } +}
a6e412bded7afix: limit the nesting depth in JSONML
3 files changed · +322 −59
src/main/java/org/json/JSONML.java+94 −19 modified@@ -27,7 +27,32 @@ private static Object parse( XMLTokener x, boolean arrayForm, JSONArray ja, - boolean keepStrings + boolean keepStrings, + int currentNestingDepth + ) throws JSONException { + return parse(x,arrayForm, ja, + keepStrings ? XMLtoJSONMLParserConfiguration.KEEP_STRINGS : XMLtoJSONMLParserConfiguration.ORIGINAL, + currentNestingDepth); + } + + /** + * Parse XML values and store them in a JSONArray. + * @param x The XMLTokener containing the source string. + * @param arrayForm true if array form, false if object form. + * @param ja The JSONArray that is containing the current tag or null + * if we are at the outermost level. + * @param config The XML parser configuration: + * XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour; + * XMLtoJSONMLParserConfiguration.KEEP_STRINGS means Don't type-convert text nodes and attribute values. + * @return A JSONArray if the value is the outermost tag, otherwise null. + * @throws JSONException if a parsing error occurs + */ + private static Object parse( + XMLTokener x, + boolean arrayForm, + JSONArray ja, + XMLtoJSONMLParserConfiguration config, + int currentNestingDepth ) throws JSONException { String attribute; char c; @@ -152,7 +177,7 @@ private static Object parse( if (!(token instanceof String)) { throw x.syntaxError("Missing value"); } - newjo.accumulate(attribute, keepStrings ? ((String)token) :XML.stringToValue((String)token)); + newjo.accumulate(attribute, config.isKeepStrings() ? ((String)token) :XML.stringToValue((String)token)); token = null; } else { newjo.accumulate(attribute, ""); @@ -181,7 +206,12 @@ private static Object parse( if (token != XML.GT) { throw x.syntaxError("Misshaped tag"); } - closeTag = (String)parse(x, arrayForm, newja, keepStrings); + + if (currentNestingDepth == config.getMaxNestingDepth()) { + throw x.syntaxError("Maximum nesting depth of " + config.getMaxNestingDepth() + " reached"); + } + + closeTag = (String)parse(x, arrayForm, newja, config, currentNestingDepth + 1); if (closeTag != null) { if (!closeTag.equals(tagName)) { throw x.syntaxError("Mismatched '" + tagName + @@ -203,7 +233,7 @@ private static Object parse( } else { if (ja != null) { ja.put(token instanceof String - ? keepStrings ? XML.unescape((String)token) :XML.stringToValue((String)token) + ? (config.isKeepStrings() ? XML.unescape((String)token) : XML.stringToValue((String)token)) : token); } } @@ -224,7 +254,7 @@ private static Object parse( * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, false); + return (JSONArray)parse(new XMLTokener(string), true, null, XMLtoJSONMLParserConfiguration.ORIGINAL, 0); } @@ -235,8 +265,8 @@ public static JSONArray toJSONArray(String string) throws JSONException { * attributes, then the second element will be JSONObject containing the * name/value pairs. If the tag contains children, then strings and * JSONArrays will represent the child tags. - * As opposed to toJSONArray this method does not attempt to convert - * any text node or attribute value to any type + * As opposed to toJSONArray this method does not attempt to convert + * any text node or attribute value to any type * but just leaves it as a string. * Comments, prologs, DTDs, and <pre>{@code <[ [ ]]>}</pre> are ignored. * @param string The source string. @@ -246,7 +276,7 @@ public static JSONArray toJSONArray(String string) throws JSONException { * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(String string, boolean keepStrings) throws JSONException { - return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings); + return (JSONArray)parse(new XMLTokener(string), true, null, keepStrings, 0); } @@ -257,8 +287,8 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J * attributes, then the second element will be JSONObject containing the * name/value pairs. If the tag contains children, then strings and * JSONArrays will represent the child content and tags. - * As opposed to toJSONArray this method does not attempt to convert - * any text node or attribute value to any type + * As opposed to toJSONArray this method does not attempt to convert + * any text node or attribute value to any type * but just leaves it as a string. * Comments, prologs, DTDs, and <pre>{@code <[ [ ]]>}</pre> are ignored. * @param x An XMLTokener. @@ -268,7 +298,7 @@ public static JSONArray toJSONArray(String string, boolean keepStrings) throws J * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JSONException { - return (JSONArray)parse(x, true, null, keepStrings); + return (JSONArray)parse(x, true, null, keepStrings, 0); } @@ -285,7 +315,7 @@ public static JSONArray toJSONArray(XMLTokener x, boolean keepStrings) throws JS * @throws JSONException Thrown on error converting to a JSONArray */ public static JSONArray toJSONArray(XMLTokener x) throws JSONException { - return (JSONArray)parse(x, true, null, false); + return (JSONArray)parse(x, true, null, false, 0); } @@ -303,10 +333,10 @@ public static JSONArray toJSONArray(XMLTokener x) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, false); + return (JSONObject)parse(new XMLTokener(string), false, null, false, 0); } - - + + /** * Convert a well-formed (but not necessarily valid) XML string into a * JSONObject using the JsonML transform. Each XML tag is represented as @@ -323,10 +353,32 @@ public static JSONObject toJSONObject(String string) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(String string, boolean keepStrings) throws JSONException { - return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings); + return (JSONObject)parse(new XMLTokener(string), false, null, keepStrings, 0); } - + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and <pre>{@code <[ [ ]]>}</pre> are ignored. + * @param string The XML source text. + * @param config The XML parser configuration: + * XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour; + * XMLtoJSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(String string, XMLtoJSONMLParserConfiguration config) throws JSONException { + return (JSONObject)parse(new XMLTokener(string), false, null, config, 0); + } + + /** * Convert a well-formed (but not necessarily valid) XML string into a * JSONObject using the JsonML transform. Each XML tag is represented as @@ -341,7 +393,7 @@ public static JSONObject toJSONObject(String string, boolean keepStrings) throws * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x) throws JSONException { - return (JSONObject)parse(x, false, null, false); + return (JSONObject)parse(x, false, null, false, 0); } @@ -361,7 +413,29 @@ public static JSONObject toJSONObject(XMLTokener x) throws JSONException { * @throws JSONException Thrown on error converting to a JSONObject */ public static JSONObject toJSONObject(XMLTokener x, boolean keepStrings) throws JSONException { - return (JSONObject)parse(x, false, null, keepStrings); + return (JSONObject)parse(x, false, null, keepStrings, 0); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and <pre>{@code <[ [ ]]>}</pre> are ignored. + * @param x An XMLTokener of the XML source text. + * @param config The XML parser configuration: + * XMLtoJSONMLParserConfiguration.ORIGINAL is the default behaviour; + * XMLtoJSONMLParserConfiguration.KEEP_STRINGS means values will not be coerced into boolean + * or numeric values and will instead be left as strings + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException Thrown on error converting to a JSONObject + */ + public static JSONObject toJSONObject(XMLTokener x, XMLtoJSONMLParserConfiguration config) throws JSONException { + return (JSONObject)parse(x, false, null, config, 0); } @@ -442,6 +516,7 @@ public static String toString(JSONArray ja) throws JSONException { return sb.toString(); } + /** * Reverse the JSONML transformation, making an XML text from a JSONObject. * The JSONObject must contain a "tagName" property. If it has children,
src/main/java/org/json/XMLtoJSONMLParserConfiguration.java+128 −0 added@@ -0,0 +1,128 @@ +package org.json; +/* +Public Domain. +*/ + +/** + * Configuration object for the XML to JSONML parser. The configuration is immutable. + */ +@SuppressWarnings({""}) +public class XMLtoJSONMLParserConfiguration { + /** + * Used to indicate there's no defined limit to the maximum nesting depth when parsing a XML + * document to JSONML. + */ + public static final int UNDEFINED_MAXIMUM_NESTING_DEPTH = -1; + + /** + * The default maximum nesting depth when parsing a XML document to JSONML. + */ + public static final int DEFAULT_MAXIMUM_NESTING_DEPTH = 512; + + /** Original Configuration of the XML to JSONML Parser. */ + public static final XMLtoJSONMLParserConfiguration ORIGINAL + = new XMLtoJSONMLParserConfiguration(); + /** Original configuration of the XML to JSONML Parser except that values are kept as strings. */ + public static final XMLtoJSONMLParserConfiguration KEEP_STRINGS + = new XMLtoJSONMLParserConfiguration().withKeepStrings(true); + + /** + * When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + */ + private boolean keepStrings; + + /** + * The maximum nesting depth when parsing a XML document to JSONML. + */ + private int maxNestingDepth = DEFAULT_MAXIMUM_NESTING_DEPTH; + + /** + * Default parser configuration. Does not keep strings (tries to implicitly convert values). + */ + public XMLtoJSONMLParserConfiguration() { + this.keepStrings = false; + } + + /** + * Configure the parser string processing and use the default CDATA Tag Name as "content". + * @param keepStrings <code>true</code> to parse all values as string. + * <code>false</code> to try and convert XML string values into a JSON value. + * @param maxNestingDepth <code>int</code> to limit the nesting depth + */ + public XMLtoJSONMLParserConfiguration(final boolean keepStrings, final int maxNestingDepth) { + this.keepStrings = keepStrings; + this.maxNestingDepth = maxNestingDepth; + } + + /** + * Provides a new instance of the same configuration. + */ + @Override + protected XMLtoJSONMLParserConfiguration clone() { + // future modifications to this method should always ensure a "deep" + // clone in the case of collections. i.e. if a Map is added as a configuration + // item, a new map instance should be created and if possible each value in the + // map should be cloned as well. If the values of the map are known to also + // be immutable, then a shallow clone of the map is acceptable. + return new XMLtoJSONMLParserConfiguration( + this.keepStrings, + this.maxNestingDepth + ); + } + + /** + * When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @return The <code>keepStrings</code> configuration value. + */ + public boolean isKeepStrings() { + return this.keepStrings; + } + + /** + * When parsing the XML into JSONML, specifies if values should be kept as strings (<code>true</code>), or if + * they should try to be guessed into JSON values (numeric, boolean, string) + * + * @param newVal + * new value to use for the <code>keepStrings</code> configuration option. + * + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLtoJSONMLParserConfiguration withKeepStrings(final boolean newVal) { + XMLtoJSONMLParserConfiguration newConfig = this.clone(); + newConfig.keepStrings = newVal; + return newConfig; + } + + /** + * The maximum nesting depth that the parser will descend before throwing an exception + * when parsing the XML into JSONML. + * @return the maximum nesting depth set for this configuration + */ + public int getMaxNestingDepth() { + return maxNestingDepth; + } + + /** + * Defines the maximum nesting depth that the parser will descend before throwing an exception + * when parsing the XML into JSONML. The default max nesting depth is 512, which means the parser + * will throw a JsonException if the maximum depth is reached. + * Using any negative value as a parameter is equivalent to setting no limit to the nesting depth, + * which means the parses will go as deep as the maximum call stack size allows. + * @param maxNestingDepth the maximum nesting depth allowed to the XML parser + * @return The existing configuration will not be modified. A new configuration is returned. + */ + public XMLtoJSONMLParserConfiguration withMaxNestingDepth(int maxNestingDepth) { + XMLtoJSONMLParserConfiguration newConfig = this.clone(); + + if (maxNestingDepth > UNDEFINED_MAXIMUM_NESTING_DEPTH) { + newConfig.maxNestingDepth = maxNestingDepth; + } else { + newConfig.maxNestingDepth = UNDEFINED_MAXIMUM_NESTING_DEPTH; + } + + return newConfig; + } +}
src/test/java/org/json/junit/JSONMLTest.java+100 −40 modified@@ -11,19 +11,19 @@ /** * 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 void emptyXMLException() { /** * 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 void nullJSONXMLException() { /** * Attempts to call JSONML.toString() with a null JSONArray. - * Expects a JSONException. + * Expects a JSONException. */ @Test public void emptyJSONXMLException() { @@ -125,7 +125,7 @@ public void emptyTagException() { "[\"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 void spaceInTagException() { } /** - * 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 void invalidSlashInTagException() { * 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 void invalidSlashInTagException() { */ @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 void invalidBangNoCloseInTagException() { * 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 void noCloseStartTagException() { * 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 void noCloseEndTagException() { * 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 void noCloseEndBraceException() { * 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 void invalidCDATABangInTagException() { /** * 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 void invalidCDATABangInTagException() { /** * 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 void toJSONArray() { * 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 void toJSONArray() { "<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 void toJSONArray() { } /** - * 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 void toJSONObjectToJSONArray() { /** * 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 void toJSONObjectToJSONArray() { "\"tagName\":\"addresses\""+ "}"; - String expectedJSONArrayStr = + String expectedJSONArrayStr = "["+ "\"addresses\","+ "{"+ @@ -645,12 +645,12 @@ public void toJSONObjectToJSONArray() { 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 void toJSONObjectToJSONArray() { * 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 void testToJSONArray_reversibility2() { 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 void testToJSONArray_reversibility3() { 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 void testToJSONObject_reversibility() { // 1. Our XML parser does not handle generic HTML entities, only valid XML entities. Hence // 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 void testToJSONObject_reversibility() { // 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 void testToJSONObject_reversibility() { // 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 void testIssue484InfinteLoop1() { 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 void testIssue484InfinteLoop2() { 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"); + } + } + }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-3vqj-43w4-2q58ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-45688ghsaADVISORY
- github.com/dromara/hutool/commit/6a2b585de0a380e8c12016dbaa1620b69be11b8cghsaWEB
- github.com/dromara/hutool/issues/2748ghsaWEB
- github.com/dromara/hutool/releases/tag/5.8.25ghsaWEB
- github.com/stleary/JSON-java/commit/a6e412bded7a0ad605adfeca029318f184c32102ghsaWEB
- github.com/stleary/JSON-java/issues/708ghsaWEB
News mentions
0No linked articles in our index yet.