This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/budget-resolution in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 666ddc4c89ffb7e3aa0b73c3f96d5ef98471cf76 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 17 07:52:42 2026 +0100 Fix scchema resolution budget bug --- .../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); + } +}
