This is an automated email from the ASF dual-hosted git repository.
joerghoh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-xss.git
The following commit(s) were added to refs/heads/master by this push:
new 5068327 SLING-13336 missing resource limits in XML/JSON validation
5068327 is described below
commit 5068327007f2c38abc16ceb1fcaac06dbc5f09ef
Author: Joerg Hoh <[email protected]>
AuthorDate: Mon Sep 14 18:10:11 2026 +0200
SLING-13336 missing resource limits in XML/JSON validation
---
src/main/java/org/apache/sling/xss/XSSAPI.java | 3 +-
.../java/org/apache/sling/xss/impl/XSSAPIImpl.java | 66 ++++++++++++++++++++++
.../org/apache/sling/xss/impl/XSSAPIImplTest.java | 50 +++++++++++++++-
3 files changed, 116 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/sling/xss/XSSAPI.java
b/src/main/java/org/apache/sling/xss/XSSAPI.java
index 0496e5d..bdb3ffe 100644
--- a/src/main/java/org/apache/sling/xss/XSSAPI.java
+++ b/src/main/java/org/apache/sling/xss/XSSAPI.java
@@ -157,7 +157,8 @@ public interface XSSAPI {
/**
* Validate an XML string
*
- * @param xml the XML string to validate
+ * @param xml the XML string to validate; if it contains a
doctype statement, it will be considered
+ * as invalid and the {@code defaultXml} value is returned.
* @param defaultXml the default value to use if {@code xml} is {@code
null} or not valid
* @return a valid XML string
*/
diff --git a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
index c469fbd..f752ad2 100644
--- a/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
+++ b/src/main/java/org/apache/sling/xss/impl/XSSAPIImpl.java
@@ -82,6 +82,7 @@ public class XSSAPIImpl implements XSSAPI {
factory.setValidating(false);
factory.setNamespaceAware(true);
try {
+
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",
true);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",
false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities",
false);
factory.setFeature("http://xml.org/sax/features/external-general-entities",
false);
@@ -339,6 +340,17 @@ public class XSSAPIImpl implements XSSAPI {
return defaultComment;
}
+ /**
+ * Maximum object/array nesting depth accepted by {@link
#getValidJSON(String, String)}.
+ * <p>
+ * The underlying JSON provider (Johnzon, via the {@code jakarta.json}
API) parses nested
+ * objects/arrays recursively and has no built-in nesting-depth limit, so
a deeply nested document
+ * (e.g. {@code "[[[[...]]]]"}) triggers an uncaught {@link
StackOverflowError} instead of a
+ * catchable parsing exception. {@link #exceedsMaxJsonNestingDepth(String,
int)} rejects such input
+ * before it reaches the parser.
+ */
+ private static final int MAX_JSON_NESTING_DEPTH = 1000;
+
/**
* @see org.apache.sling.xss.XSSAPI#getValidJSON(String, String)
*/
@@ -351,6 +363,10 @@ public class XSSAPIImpl implements XSSAPI {
if ("".equals(json)) {
return "";
}
+ if (exceedsMaxJsonNestingDepth(json, MAX_JSON_NESTING_DEPTH)) {
+ LOGGER.warn("Rejecting JSON input that exceeds the maximum nesting
depth of {}.", MAX_JSON_NESTING_DEPTH);
+ return getValidJSON(defaultJson, "");
+ }
int curlyIx = json.indexOf("{");
int straightIx = json.indexOf("[");
if (curlyIx >= 0 && (curlyIx < straightIx || straightIx < 0)) {
@@ -415,6 +431,56 @@ public class XSSAPIImpl implements XSSAPI {
return sb.toString();
}
+ /**
+ * Returns {@code true} if {@code json} contains an object/array nesting
level deeper than
+ * {@code maxDepth}. Only structural {@code {}}/{@code []} characters
outside of string literals are
+ * counted (backslash-escaped quotes are tracked so a string is not exited
early), so a string value
+ * that merely contains bracket characters cannot trigger a false
positive. This is a cheap,
+ * non-validating scan meant only to bound recursion depth before the
input reaches the JSON parser;
+ * it does not otherwise check that {@code json} is well-formed.
+ *
+ * @param json the serialized JSON document to scan
+ * @param maxDepth the maximum accepted nesting depth
+ * @return {@code true} if the nesting depth exceeds {@code maxDepth}
+ */
+ private static boolean exceedsMaxJsonNestingDepth(@NotNull String json,
int maxDepth) {
+ int depth = 0;
+ boolean inString = false;
+ boolean escaped = false;
+ for (int i = 0; i < json.length(); i++) {
+ char c = json.charAt(i);
+ if (inString) {
+ if (escaped) {
+ escaped = false;
+ } else if (c == '\\') {
+ escaped = true;
+ } else if (c == '"') {
+ inString = false;
+ }
+ continue;
+ }
+ switch (c) {
+ case '"':
+ inString = true;
+ break;
+ case '{':
+ case '[':
+ depth++;
+ if (depth > maxDepth) {
+ return true;
+ }
+ break;
+ case '}':
+ case ']':
+ depth--;
+ break;
+ default:
+ break;
+ }
+ }
+ return false;
+ }
+
/**
* @see org.apache.sling.xss.XSSAPI#getValidXML(String, String)
*/
diff --git a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
index b5d38e7..75e8e98 100644
--- a/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
+++ b/src/test/java/org/apache/sling/xss/impl/XSSAPIImplTest.java
@@ -292,6 +292,47 @@ public class XSSAPIImplTest {
}
}
+ @Test
+ public void testGetValidJSONDeepNestingDoesNotStackOverflow() {
+ // the underlying JSON provider parses objects/arrays recursively with
no built-in nesting-depth
+ // limit, so a deeply nested document used to trigger an uncaught
StackOverflowError instead of
+ // falling back to the default value like any other malformed input
+ String deeplyNested = "[".repeat(5000) + "1" + "]".repeat(5000);
+ assertTimeoutPreemptively(
+ Duration.ofSeconds(5),
+ () -> assertEquals(RUBBISH_JSON,
xssAPI.getValidJSON(deeplyNested, RUBBISH_JSON)));
+ }
+
+ @Test
+ public void testGetValidJSONNestingWithinStringIsNotFalselyRejected() {
+ // bracket characters inside a string value are not structural nesting
and must not count
+ // towards the depth limit
+ String value = "[".repeat(5000) + "]".repeat(5000);
+ String json = "{\"a\":\"" + value + "\"}";
+ String expected = "{\"a\":\"" + value + "\"}";
+ assertEquals(expected, xssAPI.getValidJSON(json, RUBBISH_JSON));
+ }
+
+ @Test
+ public void testGetValidJSONEscapedQuoteInStringIsNotFalselyRejected() {
+ // an escaped quote inside a string value must not be mistaken for the
string's closing quote -
+ // otherwise the brackets that follow it would be (wrongly) treated as
structural nesting and
+ // trip the depth limit
+ String bracketRun = "[".repeat(5000) + "]".repeat(5000);
+ String json = "{\"a\":\"\\\"" + bracketRun + "\"}";
+ String result = xssAPI.getValidJSON(json, RUBBISH_JSON);
+ assertFalse(
+ RUBBISH_JSON.equals(result), "Expected the JSON to be parsed
instead of falling back to the default");
+ }
+
+ @Test
+ public void testGetValidJSONMixedBracketTypesShareOneDepthCounter() {
+ // '{' and '[' must both feed the same depth counter - alternating
them must trip the depth
+ // limit just as reliably as repeating a single bracket type
+ String mixedNested = "{[".repeat(600);
+ assertEquals(RUBBISH_JSON, xssAPI.getValidJSON(mixedNested,
RUBBISH_JSON));
+ }
+
@ParameterizedTest
@MethodSource("dataForValidXML")
public void testGetValidXML(String source, String expected) {
@@ -883,9 +924,14 @@ public class XSSAPIImplTest {
{"<t t=\"t>test</t>", RUBBISH_XML},
{"<t><w>xyz</w></t>", "<t><w>xyz</w></t>"},
{"<t><w>xyz</t></w>", RUBBISH_XML},
+ // DOCTYPE declarations are rejected: DTDs enable internal entity
expansion attacks
+ // (billion laughs) and are not needed for a validity check on
untrusted XML
+ {"<?xml version=\"1.0\"?><!DOCTYPE test SYSTEM
\"http://nonExistentHost:1234/\"><test/>", RUBBISH_XML},
{
- "<?xml version=\"1.0\"?><!DOCTYPE test SYSTEM
\"http://nonExistentHost:1234/\"><test/>",
- "<?xml version=\"1.0\"?><!DOCTYPE test SYSTEM
\"http://nonExistentHost:1234/\"><test/>"
+ "<?xml version=\"1.0\"?><!DOCTYPE lolz [<!ENTITY lol \"lol\">"
+ + "<!ENTITY lol2 \"&lol;&lol;&lol;&lol;&lol;\">"
+ + "<!ENTITY lol3
\"&lol2;&lol2;&lol2;&lol2;&lol2;\">]><lolz>&lol3;</lolz>",
+ RUBBISH_XML
}
};
}