This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
The following commit(s) were added to refs/heads/master by this push:
new 74e892bb Bound the element depth of the schema parse (#179)
74e892bb is described below
commit 74e892bbdb720c2fa87099173ee40fcd789ccc0b
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 25 06:20:22 2026 +0100
Bound the element depth of the schema parse (#179)
---
.../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");
}