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
             }
         };
     }

Reply via email to