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 fa783350 Fix scchema resolution budget bug (#153)
fa783350 is described below
commit fa783350436892b63bf214f3e46e6b72319a71ec
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 17 07:55:54 2026 +0100
Fix scchema resolution budget bug (#153)
---
.../ws/commons/schema/XmlSchemaCollection.java | 77 ++++++++++----
.../java/tests/SchemaResolutionBudgetTest.java | 115 +++++++++++++++++++++
2 files changed, 174 insertions(+), 18 deletions(-)
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 0a367c25..0c3b0507 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
@@ -209,6 +209,14 @@ public final class XmlSchemaCollection {
private int resolutionsSinceTopLevelRead;
+ /**
+ * Depth of nested {@link #read} calls. A schema resolved through
+ * <code>xs:import</code>/<code>xs:include</code>/<code>xs:redefine</code>
is built by a
+ * re-entrant read, so this distinguishes the outermost read - where the
per-read resolution
+ * budget starts - from the nested ones that spend it.
+ */
+ private int activeReads;
+
/**
* Creates new XmlSchemaCollection
*/
@@ -539,10 +547,6 @@ public final class XmlSchemaCollection {
* @param pKey the schema key.
*/
public void push(SchemaKey pKey) {
- if (stack.isEmpty()) {
- // A new top-level read is starting.
- resolutionsSinceTopLevelRead = 0;
- }
if (stack.size() >= MAX_IMPORT_DEPTH) {
throw new XmlSchemaException("Maximum schema import/include depth
of " + MAX_IMPORT_DEPTH
+ " exceeded while resolving " + pKey
@@ -584,10 +588,15 @@ public final class XmlSchemaCollection {
* @return the schema object.
*/
public XmlSchema read(Document doc, String systemId,
TargetNamespaceValidator validator) {
- SchemaBuilder builder = new SchemaBuilder(this, validator);
- XmlSchema schema = builder.build(doc, systemId);
- schema.setInputEncoding(doc.getInputEncoding());
- return schema;
+ enterRead();
+ try {
+ SchemaBuilder builder = new SchemaBuilder(this, validator);
+ XmlSchema schema = builder.build(doc, systemId);
+ schema.setInputEncoding(doc.getInputEncoding());
+ return schema;
+ } finally {
+ exitRead();
+ }
}
/**
@@ -599,8 +608,13 @@ public final class XmlSchemaCollection {
* @return the XML schema object.
*/
public XmlSchema read(Document doc) {
- SchemaBuilder builder = new SchemaBuilder(this, null);
- return builder.build(doc, null);
+ enterRead();
+ try {
+ SchemaBuilder builder = new SchemaBuilder(this, null);
+ return builder.build(doc, null);
+ } finally {
+ exitRead();
+ }
}
/**
@@ -611,10 +625,15 @@ public final class XmlSchemaCollection {
* @return the XmlSchema
*/
public XmlSchema read(Element elem) {
- SchemaBuilder builder = new SchemaBuilder(this, null);
- XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem, null);
- xmlSchema.setInputEncoding(elem.getOwnerDocument().getXmlEncoding());
- return xmlSchema;
+ enterRead();
+ try {
+ SchemaBuilder builder = new SchemaBuilder(this, null);
+ XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem, null);
+
xmlSchema.setInputEncoding(elem.getOwnerDocument().getXmlEncoding());
+ return xmlSchema;
+ } finally {
+ exitRead();
+ }
}
/**
@@ -626,10 +645,15 @@ public final class XmlSchemaCollection {
* @return the schema object.
*/
public XmlSchema read(Element elem, String systemId) {
- SchemaBuilder builder = new SchemaBuilder(this, null);
- XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem, systemId);
- xmlSchema.setInputEncoding(elem.getOwnerDocument().getInputEncoding());
- return xmlSchema;
+ enterRead();
+ try {
+ SchemaBuilder builder = new SchemaBuilder(this, null);
+ XmlSchema xmlSchema = builder.handleXmlSchemaElement(elem,
systemId);
+
xmlSchema.setInputEncoding(elem.getOwnerDocument().getInputEncoding());
+ return xmlSchema;
+ } finally {
+ exitRead();
+ }
}
/**
@@ -818,6 +842,23 @@ public final class XmlSchemaCollection {
return schemas.get(pKey);
}
+ /**
+ * Marks the start of a read. The per-read resolution budget is reset only
for the outermost
+ * one: the stack of in-progress resolutions returns to empty between two
sibling imports of
+ * the same document, so resetting whenever it is empty charged each
branch of the import
+ * graph separately and left the total unbounded.
+ */
+ private void enterRead() {
+ if (activeReads == 0) {
+ resolutionsSinceTopLevelRead = 0;
+ }
+ activeReads++;
+ }
+
+ private void exitRead() {
+ activeReads--;
+ }
+
XmlSchema read(InputSource inputSource, TargetNamespaceValidator
namespaceValidator) {
try {
DocumentBuilderFactory docFac =
DocumentBuilderFactory.newInstance();
diff --git a/xmlschema-core/src/test/java/tests/SchemaResolutionBudgetTest.java
b/xmlschema-core/src/test/java/tests/SchemaResolutionBudgetTest.java
new file mode 100644
index 00000000..66c4a442
--- /dev/null
+++ b/xmlschema-core/src/test/java/tests/SchemaResolutionBudgetTest.java
@@ -0,0 +1,115 @@
+/**
+ * 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 tests;
+
+import java.io.StringReader;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.ws.commons.schema.XmlSchemaCollection;
+import org.apache.ws.commons.schema.XmlSchemaException;
+import org.apache.ws.commons.schema.resolver.URIResolver;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.xml.sax.InputSource;
+
+/**
+ * maxSchemaResolutions is meant to bound the number of documents one read may
pull in. The
+ * bound has to hold however the import graph is shaped: a deep chain is
already caught by
+ * maxImportDepth, so the case that needs this budget is the shallow, wide one.
+ */
+public class SchemaResolutionBudgetTest extends Assert {
+
+ /** The documented default of
org.apache.ws.commons.schema.maxSchemaResolutions. */
+ private static final int MAX_RESOLUTIONS = 1000;
+
+ /** Counts how many documents a read actually pulled in. */
+ private static final class CountingResolver implements URIResolver {
+ private final AtomicInteger count = new AtomicInteger();
+
+ public InputSource resolveEntity(String namespace, String
schemaLocation, String baseUri) {
+ int id = count.incrementAndGet();
+ InputSource source = new InputSource(new StringReader(
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"
targetNamespace=\""
+ + namespace + "\"><xs:element name=\"e\"
type=\"xs:string\"/></xs:schema>"));
+ source.setSystemId("http://example.invalid/leaf" + id + ".xsd");
+ return source;
+ }
+ }
+
+ /** Each read needs its own namespaces, or the second one collides in the
collection. */
+ private static String rootImporting(String tag, int leaves) {
+ StringBuilder root = new StringBuilder(
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"
targetNamespace=\"urn:root"
+ + tag + "\">");
+ for (int i = 0; i < leaves; i++) {
+ root.append("<xs:import
namespace=\"urn:leaf").append(tag).append('_').append(i)
+ .append("\"
schemaLocation=\"http://example.invalid/leaf").append(tag)
+ .append('_').append(i).append(".xsd\"/>");
+ }
+ return root.append("</xs:schema>").toString();
+ }
+
+ private static CountingResolver read(int leaves) {
+ XmlSchemaCollection collection = new XmlSchemaCollection();
+ CountingResolver resolver = new CountingResolver();
+ collection.setSchemaResolver(resolver);
+ collection.read(new StringReader(rootImporting("a", leaves)));
+ return resolver;
+ }
+
+ /**
+ * XMLSCHEMA-XXX: the budget was reset whenever the in-progress stack was
empty, which it is
+ * between two sibling imports of the top-level document, so a wide graph
never reached it.
+ */
+ @Test
+ public void testWideImportGraphIsBounded() {
+ CountingResolver resolver;
+ try {
+ resolver = read(MAX_RESOLUTIONS * 5);
+ fail("expected the resolution budget to stop a graph of "
+ + (MAX_RESOLUTIONS * 5) + " imports, but it resolved "
+ + resolver.count.get());
+ } catch (XmlSchemaException expected) {
+ assertTrue(expected.getMessage(),
+ expected.getMessage().contains("schema documents were
resolved"));
+ }
+ }
+
+ /** A graph inside the budget must still resolve in full. */
+ @Test
+ public void testGraphWithinBudgetStillResolves() {
+ CountingResolver resolver = read(MAX_RESOLUTIONS / 2);
+ assertEquals(MAX_RESOLUTIONS / 2, resolver.count.get());
+ }
+
+ /** The budget is per top-level read, so a second read starts from zero. */
+ @Test
+ public void testBudgetIsPerTopLevelRead() {
+ XmlSchemaCollection collection = new XmlSchemaCollection();
+ CountingResolver resolver = new CountingResolver();
+ collection.setSchemaResolver(resolver);
+ for (int i = 0; i < 3; i++) {
+ collection.read(new StringReader(rootImporting("r" + i,
MAX_RESOLUTIONS / 2)));
+ }
+ assertTrue("a later read must not inherit an earlier read's spend",
+ resolver.count.get() > MAX_RESOLUTIONS);
+ }
+}