From 6a2b585de0a380e8c12016dbaa1620b69be11b8c Mon Sep 17 00:00:00 2001 From: Looly Date: Fri, 5 Jan 2024 12:36:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0ParseConfig=EF=BC=8C=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E5=A2=9E=E5=8A=A0maxNestingDepth=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E9=81=BF=E5=85=8DStackOverflowError=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DCVE-2022-45688=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + .../src/main/java/cn/hutool/json/XML.java | 33 +++++++ .../cn/hutool/json/xml/JSONXMLParser.java | 35 ++++++-- .../java/cn/hutool/json/xml/ParseConfig.java | 88 +++++++++++++++++++ .../cn/hutool/json/xml/Issue2748Test.java | 19 ++++ 5 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 hutool-json/src/main/java/cn/hutool/json/xml/ParseConfig.java create mode 100644 hutool-json/src/test/java/cn/hutool/json/xml/Issue2748Test.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a349356397..999af7ffd4 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/hutool-json/src/main/java/cn/hutool/json/XML.java b/hutool-json/src/main/java/cn/hutool/json/XML.java index 570f20d92b..cf1c560d58 100644 --- a/hutool-json/src/main/java/cn/hutool/json/XML.java +++ b/hutool-json/src/main/java/cn/hutool/json/XML.java @@ -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 * diff --git a/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java b/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java index 6b16ca506e..135ca625ff 100644 --- a/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java +++ b/hutool-json/src/main/java/cn/hutool/json/xml/JSONXMLParser.java @@ -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")); diff --git a/hutool-json/src/main/java/cn/hutool/json/xml/ParseConfig.java b/hutool-json/src/main/java/cn/hutool/json/xml/ParseConfig.java new file mode 100644 index 0000000000..c6575d94c1 --- /dev/null +++ b/hutool-json/src/main/java/cn/hutool/json/xml/ParseConfig.java @@ -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的可选选项
+ * 参考: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; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/xml/Issue2748Test.java b/hutool-json/src/test/java/cn/hutool/json/xml/Issue2748Test.java new file mode 100644 index 0000000000..1237b36cae --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/xml/Issue2748Test.java @@ -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("", 600); + + Assert.assertThrows(JSONException.class, () -> { + XML.toJSONObject(s, ParseConfig.of().setMaxNestingDepth(512)); + }); + } +}