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

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

commit de2375f4cc80b450ad12f8ae77cb2e6932275741
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 25 09:30:51 2026 +0100

    Misc fixes
---
 README.txt                                         | 11 +++++
 SECURITY.md                                        | 10 ++++
 THREAT-MODEL.md                                    | 31 +++++++++---
 .../ws/commons/schema/XmlSchemaCollection.java     | 57 +++++++++++++---------
 .../ws/commons/schema/ElementDepthLimitTest.java   | 43 ++++++++++++++++
 .../src/test/java/tests/NestingDepthLimitTest.java | 15 ++++++
 6 files changed, 138 insertions(+), 29 deletions(-)

diff --git a/README.txt b/README.txt
index 6d11ef45..f075a5ff 100644
--- a/README.txt
+++ b/README.txt
@@ -79,6 +79,17 @@ stack. The following JVM system property adjusts the limit:
   as an entity in one - and FEATURE_SECURE_PROCESSING bounds entity
   expansion by both count and accumulated size.
 
+  The same parser also bounds the element depth of the document, including
+  markup an entity expands to, so deep markup is refused before it can
+  exhaust the thread stack. The limit is set by maxNestingDepth above:
+  twice its value plus 64, which is 1088 by default, since the schema build
+  never accepts a document deeper than that. A lower jdk.xml.maxElementDepth,
+  is kept, except that on JDK 8, 11 and 17 only a system property is seen:
+  a lower limit set just in the JDK's jaxp.properties file is raised to
+  this one there, so set -Djdk.xml.maxElementDepth instead. Raising
+  jdk.xml.maxElementDepth alone has no effect; raise maxNestingDepth
+  instead.
+
   When the DefaultURIResolver fetches a schema over http or https, the fetch
   is bounded: left to the JDK it has no timeout and no size limit, so one
   schemaLocation naming a slow or endless host can hold a parsing thread or
diff --git a/SECURITY.md b/SECURITY.md
index 80b5d4fb..f38b309c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -56,6 +56,16 @@ source must install a restricting resolver via
 See [THREAT-MODEL.md](./THREAT-MODEL.md) section 10 for the full set of
 downstream responsibilities.
 
+The parser `XmlSchemaCollection` reads schema documents with never
+resolves external DTDs or external entities, and bounds the element depth
+of what it parses, including markup that an entity expands to. That depth
+limit is set by `org.apache.ws.commons.schema.maxNestingDepth`: twice its
+value plus 64, which is 1088 by default. A lower `jdk.xml.maxElementDepth`
+is kept (on JDK 8, 11 and 17, only when set as a system property rather
+than in `jaxp.properties`), but raising `jdk.xml.maxElementDepth` alone has
+no effect. The
+library's resource limits and how to set them are listed in README.txt.
+
 A report that the bundled default resolver dereferenced an attacker-supplied
 `schemaLocation` is a documented property of that default, not a
 vulnerability in the library (THREAT-MODEL.md section 9 and section 14
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index 7a155f9b..f6acfa06 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -170,7 +170,9 @@ A finding is in-model only if it reaches a row marked 
**yes**.
   installs a no-op SAX `EntityResolver`. A DOCTYPE declaration is
   accepted: an internal DTD subset carries no external reference, and
   entity expansion stays bounded by `FEATURE_SECURE_PROCESSING` on both
-  count and accumulated size. There is no property to relax the
+  count and accumulated size. The parse is bounded in element depth too,
+  expanded entity markup included, at twice `maxNestingDepth` plus 64
+  (§5a, §8 P6). There is no property to relax the
   external-resolution controls, and none to tighten the DOCTYPE posture.
 - **`xmlschema-core` URI resolver** (`DefaultURIResolver`): in-model
   for SSRF / cross-origin fetch when the input schema is attacker-
@@ -289,7 +291,7 @@ points*:
 | `org.apache.ws.commons.schema.walker.maxDepth` system property | `256` 
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum 
depth of nested elements, model groups and substitution group members while 
walking a schema, and separately of type derivation and of attribute group 
references; a deployment running the walker on threads with small stacks (under 
about 512 KB) should lower it |
 | `org.apache.ws.commons.schema.maxImportDepth` system property | `64` 
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum 
import/include resolution depth for one schema read |
 | `org.apache.ws.commons.schema.maxSchemaResolutions` system property | `1000` 
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum 
schema documents resolved during one top-level read |
-| `org.apache.ws.commons.schema.maxNestingDepth` system property | `512` 
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum 
structural nesting depth while building the schema model, including nested 
include/import/redefine document resolutions |
+| `org.apache.ws.commons.schema.maxNestingDepth` system property | `512` 
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum 
structural nesting depth while building the schema model, including nested 
include/import/redefine document resolutions. It also sets the internal 
parser's element-depth limit, `2 × maxNestingDepth + 64` (`1088` by default, 
and never below `1`); a lower `jdk.xml.maxElementDepth` is kept (on JDK 8, 11 
and 17 only when set as a system property [...]
 | `org.apache.ws.commons.schema.remote.allowNetwork` system property | `true` 
*(documented: `README.txt`)* | operator opt-out for deployments with no remote 
schema sets | when `false`, `DefaultURIResolver` refuses a location whose 
effective scheme is `http` or `https`; local `file:` / `jar:` reads are 
unaffected, so it closes the remote-fetch half of §9's SSRF disclaimer but not 
the local-read half |
 | `org.apache.ws.commons.schema.remote.checkAddresses` system property | 
`true` *(documented: `README.txt`)* | shipped default: refuse address classes 
that never serve a schema | before a remote fetch, and again on each redirect 
hop, every address the host resolves to is checked; link-local 
(`169.254.169.254`), multicast, wildcard, IPv6 unique-local (`fd00::/7`) and 
IPv6 forms embedding one are refused. Loopback and RFC 1918 are permitted. 
Skipped when a proxy would carry the fetch, sinc [...]
 | `org.apache.ws.commons.schema.remote.maxRedirects` system property | `5` 
*(documented: `README.txt`)* | operator-tunable bound on one fetch's redirect 
chain | `DefaultURIResolver` follows redirects itself rather than leaving them 
to the JDK, so the chain is bounded, each hop is re-checked against the scheme 
and authority rules, and the chain shares one fetch deadline; `0` refuses a 
redirected location. A hop that changes scheme is refused |
@@ -465,9 +467,14 @@ leave open.
   to `XmlSchemaCollection.read(...)`.
 - **Property**: deeply nested schema structure, including depth split across
   nested include/import/redefine document resolutions, terminates with
-  `XmlSchemaException` instead of exhausting the Java thread stack.
-- **Violation symptom**: a parseable schema causes `SchemaBuilder` to recurse
-  until `StackOverflowError` or another resource-exhaustion failure.
+  `XmlSchemaException` instead of exhausting the Java thread stack. Before
+  the build starts, the internal parser refuses markup deeper than twice
+  `maxNestingDepth` plus 64, the most any schema the build accepts can
+  need, so markup an entity in the internal DTD subset expands to cannot
+  exhaust the stack inside the JDK parser either.
+- **Violation symptom**: a parseable schema causes `SchemaBuilder` or the
+  internal parser to recurse until `StackOverflowError` or another
+  resource-exhaustion failure.
 - **Severity**: **medium** availability impact when the embedding
   application accepts untrusted schemas.
 
@@ -766,6 +773,18 @@ Revise this document when any of the following lands:
   rule as first written: it tested only the URI authority, so
   `file:////host/share/x.xsd`, which parses with no authority and
   carries the host in its path instead, was not caught.
+- **2026-09-25** — a new resource limit, a revision trigger under the
+  fourth bullet above: the internal parser now bounds the element depth of
+  the document it parses, at twice `maxNestingDepth` plus 64 (#179). A
+  35 KB schema whose internal-subset entity expanded to markup a few
+  thousand elements deep overflowed the stack inside the JDK parser, before
+  `SchemaBuilder`'s own bound could see it. The limit is derived from
+  `maxNestingDepth` rather than given its own property, and never falls
+  below `1`. A lower `jdk.xml.maxElementDepth` is kept where the library
+  can see it: always on JDK 21 and later, and only as a system property on
+  JDK 8, 11 and 17, which do not report a `jaxp.properties` setting; there
+  such a setting is raised to this limit, which is still safe on ordinary
+  thread stacks. §4 reachability, §5a and §8 P6 are updated.
 - **2026-09-24** — `DefaultURIResolver` now refuses a local location that
   exists but is not a regular file. A remote fetch is bounded in time and
   bytes by the properties in §5a; a local read is handed to the parser as a
@@ -1042,7 +1061,7 @@ the JavaDoc / source comments. The project website is
 | `RELEASE-NOTE.txt` (2.3.0) | Java 17 minimum, Java 7 dropped | §5 
environment |
 | 
`xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java`
 line 361 | `org.apache.ws.commons.schema.extension_registry` system property 
loaded via `Class.forName` | §5a, §6, §11 |
 | 
`xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java` 
| `org.apache.ws.commons.schema.maxNestingDepth` structural descent limit | §5, 
§5a, §6, §8 P6 |
-| `XmlSchemaCollection.java` | internal parser sets 
`FEATURE_SECURE_PROCESSING` and disables external DTD/entity resolution 
unconditionally; DOCTYPE declarations are accepted | §5a, §8 P2 |
+| `XmlSchemaCollection.java` | internal parser sets 
`FEATURE_SECURE_PROCESSING` and disables external DTD/entity resolution 
unconditionally; DOCTYPE declarations are accepted; element depth bounded at `2 
× maxNestingDepth + 64` | §5a, §8 P2, §8 P6 |
 | `XmlSchemaCollection.java` line 745 | `AccessController.doPrivileged` 
wrapper for the SAX parse | §5 |
 | `XmlSchema.java` | serializer `TransformerFactory` sets 
`FEATURE_SECURE_PROCESSING` and disables external DTD/stylesheet access where 
supported | §5a, §8 P2 |
 | `XmlSchemaSerializer.java` lines 1566-1567 | `DocumentBuilderFactory` sets 
`FEATURE_SECURE_PROCESSING`; used only via `newDocument()`, so it never parses 
input and carries no XXE surface | §5a, §8 P2 |
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 32d21c98..7139bd4a 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
@@ -885,7 +885,7 @@ 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)) {
+            if (e.getMessage() != null && 
e.getMessage().startsWith(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 ("
@@ -935,22 +935,6 @@ public final class XmlSchemaCollection {
         return doc;
     }
 
-    /**
-     * Blocks the external-resolution half of DTD processing, which is what 
makes
-     * DTDs dangerous: external general entities, external parameter entities 
and
-     * the external DTD subset are never fetched, so a schema document cannot 
read
-     * local files or reach the network through its DOCTYPE.
-     * <p>
-     * The DOCTYPE declaration itself is accepted. An internal DTD subset is a
-     * legitimate and widely used part of real schema documents - the W3C's own
-     * normative schemas (XML Signature, XML Encryption, XKMS) declare the
-     * entities they use for their target namespace in one - and refusing it
-     * closes no attack path that is still open here: it holds no external
-     * reference, and {@code FEATURE_SECURE_PROCESSING} bounds entity expansion
-     * by both count and accumulated size, so neither nested nor flat expansion
-     * 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.
@@ -958,7 +942,7 @@ public final class XmlSchemaCollection {
     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. 
*/
+    /** The code that starts the JDK parser's message when that limit is 
exceeded. */
     private static final String ELEMENT_DEPTH_ERROR = "JAXP00010006";
 
     /**
@@ -969,10 +953,10 @@ public final class XmlSchemaCollection {
      * 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.
+     * JDK's own default, is left in place where it can be seen: see 
currentElementDepthLimit.
      */
     private static void limitElementDepth(DocumentBuilderFactory docFac) {
-        final long limit = Math.min(Integer.MAX_VALUE, 2L * 
SchemaBuilder.MAX_NESTING_DEPTH + 64L);
+        final long limit = elementDepthLimit(SchemaBuilder.MAX_NESTING_DEPTH);
         final long existing = currentElementDepthLimit(docFac);
         if (existing > 0 && existing <= limit) {
             return;
@@ -984,11 +968,22 @@ public final class XmlSchemaCollection {
         }
     }
 
+    /**
+     * The parser element-depth limit for a given maxNestingDepth: twice it 
plus 64, kept between
+     * 1 and Integer.MAX_VALUE so that no setting of maxNestingDepth, however 
extreme, turns the
+     * limit off (the JDK reads 0 or less as no limit).
+     */
+    static long elementDepthLimit(int maxNestingDepth) {
+        return Math.max(1L, Math.min(Integer.MAX_VALUE, 2L * maxNestingDepth + 
64L));
+    }
+
     /**
      * 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.
+     * the factory: JDK 21 does; JDK 8 and 11 throw 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. On those JDKs a limit set only in the JDK's jaxp.properties 
file is not seen, and
+     * is raised to this library's own.
      */
     private static long currentElementDepthLimit(DocumentBuilderFactory 
docFac) {
         Object current = null;
@@ -1007,6 +1002,22 @@ public final class XmlSchemaCollection {
         return getIntProperty("jdk.xml.maxElementDepth", 0);
     }
 
+    /**
+     * Blocks the external-resolution half of DTD processing, which is what 
makes
+     * DTDs dangerous: external general entities, external parameter entities 
and
+     * the external DTD subset are never fetched, so a schema document cannot 
read
+     * local files or reach the network through its DOCTYPE.
+     * <p>
+     * The DOCTYPE declaration itself is accepted. An internal DTD subset is a
+     * legitimate and widely used part of real schema documents - the W3C's own
+     * normative schemas (XML Signature, XML Encryption, XKMS) declare the
+     * entities they use for their target namespace in one - and refusing it
+     * closes no attack path that is still open here: it holds no external
+     * reference, and {@code FEATURE_SECURE_PROCESSING} bounds entity expansion
+     * by both count and accumulated size, so neither nested nor flat expansion
+     * runs away.
+     * </p>
+     */
     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/org/apache/ws/commons/schema/ElementDepthLimitTest.java
 
b/xmlschema-core/src/test/java/org/apache/ws/commons/schema/ElementDepthLimitTest.java
new file mode 100644
index 00000000..3564360c
--- /dev/null
+++ 
b/xmlschema-core/src/test/java/org/apache/ws/commons/schema/ElementDepthLimitTest.java
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ws.commons.schema;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * The parser element-depth limit that XmlSchemaCollection derives from 
maxNestingDepth.
+ */
+public class ElementDepthLimitTest extends Assert {
+
+    @Test
+    public void testLimitIsTwiceTheNestingDepthPlus64() {
+        assertEquals(1088L, XmlSchemaCollection.elementDepthLimit(512));
+    }
+
+    @Test
+    public void testExtremeNestingDepthDoesNotDisableTheLimit() {
+        assertEquals(30L, XmlSchemaCollection.elementDepthLimit(-17));
+        assertEquals(1L, XmlSchemaCollection.elementDepthLimit(-32));
+        assertEquals(1L, XmlSchemaCollection.elementDepthLimit(-33));
+        assertEquals(1L, 
XmlSchemaCollection.elementDepthLimit(Integer.MIN_VALUE));
+        assertEquals(Integer.MAX_VALUE, 
XmlSchemaCollection.elementDepthLimit(Integer.MAX_VALUE));
+    }
+}
diff --git a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java 
b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
index 68d87ed5..cb4a0a05 100644
--- a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
+++ b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
@@ -273,6 +273,21 @@ public class NestingDepthLimitTest extends Assert {
         }
     }
 
+    /**
+     * Only the parser's own depth error is reworded: a malformed document 
whose element is named
+     * after the error code keeps the parser's message.
+     */
+    @Test
+    public void testUnrelatedErrorNamingTheDepthCodeIsNotReworded() throws 
Exception {
+        try {
+            new XmlSchemaCollection().read(new StringReader(
+                "<xs:schema 
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";><JAXP00010006></xs:schema>"));
+            fail("A malformed schema should be refused.");
+        } catch (XmlSchemaException expected) {
+            assertFalse(expected.getMessage(), 
expected.getMessage().contains("nested too deeply"));
+        }
+    }
+
     private static void assertRejectedAsNested(String schema) {
         try {
             new XmlSchemaCollection().read(new StringReader(schema));

Reply via email to