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