This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/annotation-bound in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit d2c9425b66ddd80fc70636971dca647865a7ee5b Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 24 10:53:22 2026 +0100 Bound the nesting depth of annotation markup --- .../apache/ws/commons/schema/SchemaBuilder.java | 46 ++++++++++++++++++++ .../src/test/java/tests/NestingDepthLimitTest.java | 50 ++++++++++++++++++++++ 2 files changed, 96 insertions(+) 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 e7609d9f..d038cad2 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 @@ -151,6 +151,50 @@ public class SchemaBuilder { collection.builderNestingDepth--; } + /** + * Refuse appinfo or documentation markup nested deeper than the structural + * bound allows. The markup is not schema structure, so the handlers never + * count it, but it is copied with a recursive cloneNode on this same thread + * stack, so it counts toward the same bound. Measured iteratively, as the + * point is to avoid recursing over it. + */ + private void checkMarkupDepth(Element content) { + int depth = 0; + int maxDepth = 0; + Node node = content.getFirstChild(); + if (node != null) { + depth = 1; + } + while (node != null) { + if (depth > maxDepth) { + maxDepth = depth; + if (collection.builderNestingDepth + maxDepth > MAX_NESTING_DEPTH) { + throw new XmlSchemaException("The markup of an annotation is nested more than " + + MAX_NESTING_DEPTH + " levels deep, counting the" + + " schema structure around it; refusing to build it." + + " The limit may be changed with the" + + " org.apache.ws.commons.schema.maxNestingDepth" + + " system property."); + } + } + if (node.getFirstChild() != null) { + node = node.getFirstChild(); + depth++; + continue; + } + while (node != null && node.getNextSibling() == null) { + node = node.getParentNode(); + depth--; + if (node == content) { + node = null; + } + } + if (node != null) { + node = node.getNextSibling(); + } + } + } + private static int getIntProperty(String name, int defaultValue) { try { Integer value = Integer.getInteger(name); @@ -306,6 +350,7 @@ public class SchemaBuilder { */ XmlSchemaAppInfo handleAppInfo(Element content) { XmlSchemaAppInfo appInfo = new XmlSchemaAppInfo(); + checkMarkupDepth(content); NodeList markup = new DocumentFragmentNodeList(content); if (!content.hasAttribute("source") && markup.getLength() == 0) { @@ -418,6 +463,7 @@ public class SchemaBuilder { // to collection XmlSchemaDocumentation handleDocumentation(Element content) { XmlSchemaDocumentation documentation = new XmlSchemaDocumentation(); + checkMarkupDepth(content); List<Node> markup = getChildren(content); if (!content.hasAttribute("source") && !content.hasAttribute("xml:lang") && markup == null) { diff --git a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java index d9323542..e9c70811 100644 --- a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java +++ b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java @@ -129,6 +129,56 @@ public class NestingDepthLimitTest extends Assert { assertNotNull(schema); } + /** + * Annotation markup is copied with a recursive cloneNode, so deep markup + * inside xs:documentation or xs:appinfo must hit the same bound rather + * than overflow the stack. + */ + @Test + public void testDeeplyNestedDocumentationMarkupIsRejected() throws Exception { + assertMarkupRejected("documentation"); + } + + @Test + public void testDeeplyNestedAppInfoMarkupIsRejected() throws Exception { + assertMarkupRejected("appinfo"); + } + + @Test + public void testReasonablyNestedMarkupStillParses() throws Exception { + XmlSchemaCollection collection = new XmlSchemaCollection(); + XmlSchema schema = collection.read(new StringReader(buildAnnotatedSchema("documentation", 50))); + assertNotNull(schema); + schema = collection.read(new StringReader(buildAnnotatedSchema("appinfo", 50))); + assertNotNull(schema); + } + + private void assertMarkupRejected(String kind) { + XmlSchemaCollection collection = new XmlSchemaCollection(); + try { + collection.read(new StringReader(buildAnnotatedSchema(kind, 5000))); + fail("xs:" + kind + " markup nested 5000 levels deep should be rejected."); + } catch (XmlSchemaException expected) { + assertTrue(expected.getMessage().contains("nested")); + } + } + + private String buildAnnotatedSchema(String kind, int depth) { + StringBuilder schema = new StringBuilder(); + schema.append("<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"") + .append(" targetNamespace=\"urn:markup-").append(kind).append("\">"); + schema.append("<xs:annotation><xs:").append(kind).append(">"); + for (int i = 0; i < depth; i++) { + schema.append("<a>"); + } + schema.append("text"); + for (int i = 0; i < depth; i++) { + schema.append("</a>"); + } + schema.append("</xs:").append(kind).append("></xs:annotation></xs:schema>"); + return schema.toString(); + } + private String buildNestedSchema(int depth) { return buildNestedSchema(depth, "urn:nesting"); }
