This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/schema-parse
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git

commit 1138807466d00fc6f49b7c54fe81995389badec5
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 25 06:15:33 2026 +0100

    Bound the element depth of the schema parse
---
 .../apache/ws/commons/schema/SchemaBuilder.java    |  2 +-
 .../ws/commons/schema/XmlSchemaCollection.java     | 66 ++++++++++++++++++++
 .../src/test/java/tests/NestingDepthLimitTest.java | 71 ++++++++++++++++++++++
 3 files changed, 138 insertions(+), 1 deletion(-)

diff --git 
a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java 
b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java
index 8aa39151..d23eb64f 100644
--- 
a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java
+++ 
b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java
@@ -83,7 +83,7 @@ public class SchemaBuilder {
      * counts one level toward the same bound (see resolveXmlSchema), which
      * also bounds a chain of distinct single-hop documents.
      */
-    private static final int MAX_NESTING_DEPTH =
+    static final int MAX_NESTING_DEPTH =
         getIntProperty("org.apache.ws.commons.schema.maxNestingDepth", 512);
     XmlSchemaCollection collection;
     Document currentDocument;
diff --git 
a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
 
b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
index 7c78389f..32d21c98 100644
--- 
a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
+++ 
b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java
@@ -874,6 +874,7 @@ public final class XmlSchemaCollection {
             docFac.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
Boolean.TRUE);
             docFac.setNamespaceAware(true);
             hardenAgainstDtdProcessing(docFac);
+            limitElementDepth(docFac);
             final DocumentBuilder builder = docFac.newDocumentBuilder();
             builder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
             Document doc = null;
@@ -884,6 +885,15 @@ public final class XmlSchemaCollection {
         } catch (IOException e) {
             throw new XmlSchemaException(e.getMessage(), e);
         } catch (SAXException e) {
+            if (e.getMessage() != null && 
e.getMessage().contains(ELEMENT_DEPTH_ERROR)) {
+                // Name the property that governs the limit: raising 
jdk.xml.maxElementDepth alone
+                // has no effect, as limitElementDepth() lowers it again.
+                throw new XmlSchemaException("The schema document is nested 
too deeply to parse ("
+                                             + e.getMessage() + "). The limit 
is twice the"
+                                             + " 
org.apache.ws.commons.schema.maxNestingDepth system"
+                                             + " property plus 64, unless 
jdk.xml.maxElementDepth"
+                                             + " sets a lower one.", e);
+            }
             throw new XmlSchemaException(e.getMessage(), e);
         }
     }
@@ -941,6 +951,62 @@ public final class XmlSchemaCollection {
      * runs away.
      * </p>
      */
+    /**
+     * The JDK parser's element-depth limit, 
<code>jdk.xml.maxElementDepth</code>, under the name
+     * JDK 8 onwards accepts on a factory.
+     */
+    private static final String MAX_ELEMENT_DEPTH =
+        "http://www.oracle.com/xml/jaxp/properties/maxElementDepth";;
+
+    /** The code the JDK parser's message carries when that limit is exceeded. 
*/
+    private static final String ELEMENT_DEPTH_ERROR = "JAXP00010006";
+
+    /**
+     * Bound the element depth of the parse itself. An internal DTD subset is 
accepted, and the JDK
+     * builds an entity's replacement markup recursively: a 35 KB schema whose 
entity expands to
+     * elements nested a few thousand deep overflowed the thread stack inside 
the parser, before
+     * the schema builder's own nesting bound could see it. The builder never 
accepts a document
+     * deeper than about twice its structural bound, maxNestingDepth, since 
each counted level (an
+     * element, a type, a model group) is at most two XML levels deep, so 
refusing deeper markup
+     * in the parser costs nothing. A lower limit already set, by 
jdk.xml.maxElementDepth or by the
+     * JDK's own default, is left in place.
+     */
+    private static void limitElementDepth(DocumentBuilderFactory docFac) {
+        final long limit = Math.min(Integer.MAX_VALUE, 2L * 
SchemaBuilder.MAX_NESTING_DEPTH + 64L);
+        final long existing = currentElementDepthLimit(docFac);
+        if (existing > 0 && existing <= limit) {
+            return;
+        }
+        try {
+            docFac.setAttribute(MAX_ELEMENT_DEPTH, String.valueOf(limit));
+        } catch (IllegalArgumentException e) {
+            // A parser other than the JDK's, which does not recognize the 
property.
+        }
+    }
+
+    /**
+     * The element-depth limit already in force, or 0 for none. Only some JDKs 
report it through
+     * the factory: JDK 21 does; JDK 8 throws IllegalArgumentException, and 
JDK 17 reports only
+     * attributes set on the factory itself, returning null or throwing 
NullPointerException. When
+     * the factory gives no value, the system property the parser takes it 
from is read instead.
+     */
+    private static long currentElementDepthLimit(DocumentBuilderFactory 
docFac) {
+        Object current = null;
+        try {
+            current = docFac.getAttribute(MAX_ELEMENT_DEPTH);
+        } catch (RuntimeException e) {
+            // Not reported by this JDK's factory.
+        }
+        if (current != null) {
+            try {
+                return Long.parseLong(current.toString().trim());
+            } catch (NumberFormatException e) {
+                // Fall back to the system property.
+            }
+        }
+        return getIntProperty("jdk.xml.maxElementDepth", 0);
+    }
+
     private static void hardenAgainstDtdProcessing(DocumentBuilderFactory 
docFac) {
         trySetFeature(docFac, 
"http://xml.org/sax/features/external-general-entities";, false);
         trySetFeature(docFac, 
"http://xml.org/sax/features/external-parameter-entities";, false);
diff --git a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java 
b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
index de91c742..68d87ed5 100644
--- a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
+++ b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
@@ -219,6 +219,77 @@ public class NestingDepthLimitTest extends Assert {
         return schema.toString();
     }
 
+    /**
+     * An internal DTD subset is accepted, and the JDK parser builds an 
entity's replacement markup
+     * recursively, so a small document whose entity expands to deeply nested 
elements used to
+     * overflow the stack inside the parser, before the schema builder saw any 
of it.
+     */
+    @Test
+    public void testEntityExpandingToDeepMarkupIsRejected() throws Exception {
+        assertRejectedAsNested("<!DOCTYPE xs:schema [<!ENTITY deep \"" + 
repeat("<m>", 5000)
+                               + repeat("</m>", 5000) + "\">]>"
+                               + "<xs:schema 
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>&deep;</xs:schema>");
+    }
+
+    @Test
+    public void testNestedEntitiesExpandingToDeepMarkupAreRejected() throws 
Exception {
+        StringBuilder dtd = new StringBuilder("<!DOCTYPE xs:schema [<!ENTITY 
e0 \"<m/>\">");
+        for (int i = 1; i < 100; i++) {
+            dtd.append("<!ENTITY e").append(i).append(" 
\"").append(repeat("<m>", 50)).append("&e")
+                .append(i - 1).append(';').append(repeat("</m>", 
50)).append("\">");
+        }
+        dtd.append("]>");
+        assertRejectedAsNested(dtd + "<xs:schema 
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>&e99;"
+                               + "</xs:schema>");
+    }
+
+    /**
+     * Each counted level of structure is at most two XML levels deep, so the 
deepest schema the
+     * builder accepts is still parsed: here 510 nested anonymous simple 
types, 1025 elements deep.
+     */
+    @Test
+    public void testDeepestAcceptedSchemaStillParses() throws Exception {
+        StringBuilder schema = new StringBuilder("<xs:schema 
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"";
+                                                 + " 
targetNamespace=\"urn:deepest\">"
+                                                 + "<xs:simpleType 
name=\"T\"><xs:restriction>");
+        schema.append(repeat("<xs:simpleType><xs:restriction>", 510))
+            .append("<xs:simpleType><xs:restriction 
base=\"xs:string\"/></xs:simpleType>")
+            .append(repeat("</xs:restriction></xs:simpleType>", 510))
+            .append("</xs:restriction></xs:simpleType></xs:schema>");
+        assertNotNull(new XmlSchemaCollection().read(new 
StringReader(schema.toString())));
+    }
+
+    /**
+     * A lower element-depth limit already in force is kept rather than raised.
+     */
+    @Test
+    public void testLowerParserDepthLimitIsKept() throws Exception {
+        System.setProperty(MAX_ELEMENT_DEPTH, "20");
+        try {
+            new XmlSchemaCollection().read(new 
StringReader(buildNestedSchema(10)));
+            fail("A schema 30 elements deep should be refused under a parser 
limit of 20.");
+        } catch (XmlSchemaException expected) {
+            assertTrue(expected.getMessage(), 
expected.getMessage().contains("nested"));
+        }
+    }
+
+    private static void assertRejectedAsNested(String schema) {
+        try {
+            new XmlSchemaCollection().read(new StringReader(schema));
+            fail("The deeply nested schema should be rejected.");
+        } catch (XmlSchemaException expected) {
+            assertTrue(expected.getMessage(), 
expected.getMessage().contains("nested"));
+        }
+    }
+
+    private static String repeat(String s, int count) {
+        StringBuilder b = new StringBuilder(s.length() * count);
+        for (int i = 0; i < count; i++) {
+            b.append(s);
+        }
+        return b.toString();
+    }
+
     private String buildNestedSchema(int depth) {
         return buildNestedSchema(depth, "urn:nesting");
     }

Reply via email to