This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/import-depth-limit
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git

commit c03d3d26ba4b3a9f170afc6f651176a1ff72b556
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Tue Aug 25 11:58:04 2026 +0100

    Impose an import depth limit
---
 README.txt                                         | 13 ++++
 THREAT-MODEL.md                                    | 50 +++++++-----
 .../ws/commons/schema/XmlSchemaCollection.java     | 51 ++++++++++++
 .../src/test/java/tests/ImportDepthLimitTest.java  | 90 ++++++++++++++++++++++
 4 files changed, 184 insertions(+), 20 deletions(-)

diff --git a/README.txt b/README.txt
index d67f1402..218ce58b 100644
--- a/README.txt
+++ b/README.txt
@@ -39,10 +39,23 @@ adjust the per-document limits:
       Maximum previously traversed events replayed while backtracking. The
       default is 1000000.
 
+  The schema collection limits the work performed while resolving imported and
+  included schemas. The following JVM system properties adjust the limits:
+
+    org.apache.ws.commons.schema.maxImportDepth
+      Maximum import/include resolution depth for a schema read. The default
+      is 64.
+
+    org.apache.ws.commons.schema.maxSchemaResolutions
+      Maximum number of schema documents resolved during a single top-level
+      read. The default is 1000.
+
 For example, set a limit with:
 
   -Dorg.apache.ws.commons.schema.walker.maxDecisionPoints=20000
 
+    -Dorg.apache.ws.commons.schema.maxImportDepth=128
+
 ===================
       Support
 ===================
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index 4dc3afaa..3a9be679 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -213,10 +213,13 @@ A finding is in-model only if it reaches a row marked 
**yes**.
   sanitization of `schemaLocation` values that begin with `file://`
   *(inferred — §14 Q7)*.
 - **Memory**: schemas are held in memory; XMLSchema has no built-in
-  ceiling on schema-document size, number of imports, or import-graph
-  depth *(inferred — §14 Q8)*. The walker has active-path cycle detection
+  ceiling on schema-document size, imported bytes, or fetch rate. Import
+  and include resolution is bounded per read by a default maximum depth of
+  64 and maximum of 1000 resolved schema documents; both are configurable
+  with JVM system properties. The walker has active-path cycle detection
   for the schema expansion graphs it traverses, but large acyclic schemas
-  may still consume substantial memory and CPU.
+  may still consume substantial memory and CPU *(documented:
+  `README.txt`)*.
 - **System properties**: `org.apache.ws.commons.schema.extension_registry`
   is consulted at `XmlSchemaCollection` construction time, and the
   named class is loaded via `Class.forName()` *(documented:
@@ -257,6 +260,8 @@ points*:
 | `XmlSchemaCollection.setBaseUri(String)` | unset *(documented)* | 
caller-supplied | base URI against which relative `schemaLocation` values 
resolve |
 | `org.apache.ws.commons.schema.walker.maxDecisionPoints` system property | 
`10000` *(documented: `XmlSchemaPathFinder.java`)* | operator-tunable 
per-process limit | maximum decision points created while matching one document 
|
 | `org.apache.ws.commons.schema.walker.maxReplayedEvents` system property | 
`1000000` *(documented: `XmlSchemaPathFinder.java`)* | operator-tunable 
per-process limit | maximum previously traversed events replayed while 
backtracking through one document |
+| `org.apache.ws.commons.schema.maxImportDepth` system property | `64` 
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum 
import/include resolution depth for one schema read |
+| `org.apache.ws.commons.schema.maxSchemaResolutions` system property | `1000` 
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum 
schema documents resolved during one top-level read |
 | `DocumentBuilderFactory` provider | JDK default (typically Xerces fork) 
*(inferred — §14 Q6)* | depends on the JDK | shape of XML parsing for 
`read(InputSource)` / `read(InputStream)` paths |
 
 ### The insecure-default case
@@ -304,8 +309,10 @@ feature is the intended defense and is sufficient) or 
`MODEL-GAP`
 ### Size / shape / rate
 
 - No documented limit on schema-document size *(inferred — §14 Q8)*.
-- No documented limit on the import-graph depth or breadth
-  *(inferred — §14 Q8)*.
+- Import/include resolution has a default maximum depth of 64 and a
+  default maximum of 1000 resolved schema documents per top-level read;
+  both limits are configurable with JVM system properties *(documented:
+  `README.txt`)*.
 - Walker expansion cycles are rejected for type derivation, substitution
   groups, model groups, and attribute groups. This prevents recursive
   stack exhaustion for malformed but parseable schemas; it is not a general
@@ -424,10 +431,11 @@ matching disclaimer.
   `Element`.** The hardening on the internal `DocumentBuilderFactory`
   is moot — the caller's parser produced the DOM *(documented:
   `XmlSchemaCollection.read(Document)` / `.read(Element)`)*.
-- **No bound on schema-document size, import-graph depth, or import
-  fan-out.** A schema that includes thousands of imports, or imports
-  recursively, will be processed to completion or until JVM resources
-  are exhausted *(inferred — §14 Q8)*.
+- **No bound on schema-document size, imported bytes, or fetch rate.**
+  Import/include depth and total resolved documents per top-level read are
+  bounded, but a schema can still consume substantial resources within
+  those limits *(inferred — §14 Q8; depth and resolution limits documented
+  in `README.txt`)*.
 - **No protection of imported schemas at rest.** Schemas pulled from
   HTTP are fetched in cleartext if the URL is `http://`. The caller
   must use TLS-protected URLs or install a restricting resolver
@@ -499,9 +507,10 @@ The embedding Java application **must**:
    against attacker-controlled bytes, verify the JDK XML provider in
    use treats `FEATURE_SECURE_PROCESSING=true` as sufficient defense
    *(inferred — §14 Q6)*.
-4. Bound the maximum allowable schema size and the maximum import-graph
-   depth at the *caller* level. XMLSchema imposes no such limit
-   *(inferred — §14 Q8)*.
+4. Bound maximum schema size, imported bytes, and fetch rate at the
+  *caller* level. XMLSchema provides configurable import/include depth
+  and per-read resolution limits, but these do not replace deployment-
+  specific resource budgets *(inferred — §14 Q8)*.
 5. Set `org.apache.ws.commons.schema.extension_registry` only at JVM
    startup from a trusted source; do not allow untrusted actors to set
    it.
@@ -520,9 +529,9 @@ defense-in-depth controls:
   `FEATURE_SECURE_PROCESSING=true`: `disallow-doctype-decl=true`,
   `external-general-entities=false`, and
   `external-parameter-entities=false`.
-3. Apply import-fetch budgets at the caller boundary: maximum import
-  depth, total imported bytes, and total import count per top-level
-  parse.
+3. Supplement XMLSchema's import/include depth and per-read resolution
+  limits with caller-boundary budgets for total imported bytes and fetch
+  rate per top-level parse.
 4. Use connect/read timeouts for import fetches and fail closed on
   timeout or policy-check errors.
 5. Log import-resolution decisions (requested URI, normalized target,
@@ -640,7 +649,7 @@ A report against XMLSchema receives exactly one of the 
following:
 | `OUT-OF-MODEL: unsupported-component` | Lands in `w3c-testcases/`, 
`*/src/test/`, `etc/`, `xmlschema-bundle-test/`. | §3 items 4, 8 |
 | `OUT-OF-MODEL: non-default-build` | Only manifests under a §5a configuration 
the maintainer rules dev/test (e.g. an unsafe custom `URIResolver`). | §5a |
 | `OUT-OF-MODEL: out-of-layer` | Concerns a *document* validation step 
delegated to `javax.xml.validation.Validator`, or a WSDL parser upstream. | §3 
items 1–3 |
-| `BY-DESIGN: property-disclaimed` | Concerns a §9 property the project 
explicitly does not provide (no SSRF defense, no XXE defense beyond 
secure-processing, no schema-size or import-graph resource ceiling). | §9 |
+| `BY-DESIGN: property-disclaimed` | Concerns a §9 property the project 
explicitly does not provide (no SSRF defense, no XXE defense beyond 
secure-processing, no schema-size, imported-byte, or fetch-rate ceiling). | §9 |
 | `KNOWN-NON-FINDING` | Matches a §11a recurring false positive. | §11a |
 | `MODEL-GAP` | Cannot be cleanly routed to any of the above — triggers §12 
model revision. | §12 |
 
@@ -702,10 +711,11 @@ inheritance from JDK Xerces.** *(maps to §5a, §8 P2, §9, 
§11a)*
 when it begins with `file://`, `jar:`, etc. (proposed: no sanitization;
 operator's `URIResolver` is the gate). *(maps to §5, §11a)*
 
-**Q8.** No documented bound on schema-document size or
-import-graph depth (proposed: confirm "no bound, operator's
-responsibility to cap"). Are there *de facto* bounds inside XMLSchema?
-*(maps to §5, §9, §10 item 4)*
+**Q8.** XMLSchema has no documented bound on schema-document size,
+imported bytes, or fetch rate. Import/include resolution does have
+configurable defaults of 64 levels and 1000 resolved schema documents per
+top-level read. The caller remains responsible for additional deployment-
+specific resource budgets. *(maps to §5, §9, §10 item 4)*
 
 **Q9.** `org.apache.ws.commons.schema.extension_registry` system
 property: confirm that production deployments are expected to set
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 ba78332e..0e5b32aa 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
@@ -165,6 +165,21 @@ public final class XmlSchemaCollection {
      */
     private Map<SchemaKey, XmlSchema> schemas;
 
+    /**
+     * Maximum depth of the import/include resolution stack, and maximum
+     * number of schema resolutions per top-level read. Both bound the total
+     * work a single parse can trigger regardless of what fetched documents
+     * contain: exact-key cycle detection alone is defeated by per-level
+     * variation of the schema location (for example an ever-changing query
+     * string), which otherwise recurses until the thread stack overflows.
+     */
+    private static final int MAX_IMPORT_DEPTH =
+        getIntProperty("org.apache.ws.commons.schema.maxImportDepth", 64);
+    private static final int MAX_RESOLUTIONS_PER_READ =
+        getIntProperty("org.apache.ws.commons.schema.maxSchemaResolutions", 
1000);
+
+    private int resolutionsSinceTopLevelRead;
+
     /**
      * Creates new XmlSchemaCollection
      */
@@ -480,6 +495,26 @@ 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
+                                         + "; the import graph is too deep or 
does not terminate."
+                                         + " The limit may be changed with the"
+                                         + " 
org.apache.ws.commons.schema.maxImportDepth system property.");
+        }
+        resolutionsSinceTopLevelRead++;
+        if (resolutionsSinceTopLevelRead > MAX_RESOLUTIONS_PER_READ) {
+            throw new XmlSchemaException("More than " + 
MAX_RESOLUTIONS_PER_READ
+                                         + " schema documents were resolved by 
a single read; the import"
+                                         + " graph is too large or does not 
terminate. The limit may be"
+                                         + " changed with the"
+                                         + " 
org.apache.ws.commons.schema.maxSchemaResolutions"
+                                         + " system property.");
+        }
         stack.push(pKey);
     }
 
@@ -858,4 +893,20 @@ public final class XmlSchemaCollection {
         return null;
     }
 
+    private static int getIntProperty(final String name, int defaultValue) {
+        try {
+            String value = AccessController.doPrivileged(new 
PrivilegedAction<String>() {
+                public String run() {
+                    return System.getProperty(name);
+                }
+            });
+            if (value != null) {
+                return Integer.parseInt(value);
+            }
+        } catch (RuntimeException e) {
+            // fall through to the default
+        }
+        return defaultValue;
+    }
+
 }
diff --git a/xmlschema-core/src/test/java/tests/ImportDepthLimitTest.java 
b/xmlschema-core/src/test/java/tests/ImportDepthLimitTest.java
new file mode 100644
index 00000000..f77fdf3a
--- /dev/null
+++ b/xmlschema-core/src/test/java/tests/ImportDepthLimitTest.java
@@ -0,0 +1,90 @@
+/**
+ * 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.File;
+import java.io.FileWriter;
+import java.io.Writer;
+
+import org.apache.ws.commons.schema.XmlSchema;
+import org.apache.ws.commons.schema.XmlSchemaCollection;
+import org.apache.ws.commons.schema.XmlSchemaException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.xml.sax.InputSource;
+
+/**
+ * The total work of a single read must be bounded: an include chain
+ * deeper than the configured maximum depth is rejected with the documented
+ * XmlSchemaException rather than recursing until the thread stack overflows.
+ */
+public class ImportDepthLimitTest extends Assert {
+
+    private File createIncludeChain(File dir, int depth) throws Exception {
+        for (int i = 0; i < depth; i++) {
+            File f = new File(dir, "chain" + i + ".xsd");
+            Writer out = new FileWriter(f);
+            try {
+                out.write("<xs:schema 
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"";);
+                out.write(" targetNamespace=\"urn:deep\">");
+                if (i + 1 < depth) {
+                    out.write("<xs:include schemaLocation=\"chain" + (i + 1) + 
".xsd\"/>");
+                }
+                out.write("</xs:schema>");
+            } finally {
+                out.close();
+            }
+        }
+        return new File(dir, "chain0.xsd");
+    }
+
+    private File newTempDir() {
+        File dir = new File(System.getProperty("java.io.tmpdir"),
+                            "xmlschema-depth-" + System.nanoTime());
+        assertTrue(dir.mkdir());
+        return dir;
+    }
+
+    @Test
+    public void testShallowIncludeChainStillParses() throws Exception {
+        File dir = newTempDir();
+        File root = createIncludeChain(dir, 5);
+
+        XmlSchemaCollection collection = new XmlSchemaCollection();
+        XmlSchema schema = collection.read(new 
InputSource(root.toURI().toString()));
+        assertNotNull(schema);
+    }
+
+    @Test
+    public void testIncludeChainDeeperThanLimitIsRejected() throws Exception {
+        File dir = newTempDir();
+        File root = createIncludeChain(dir, 5000);
+
+        XmlSchemaCollection collection = new XmlSchemaCollection();
+        try {
+            collection.read(new InputSource(root.toURI().toString()));
+            fail("An include chain deeper than the maximum depth should be 
rejected.");
+        } catch (XmlSchemaException expected) {
+            assertTrue(expected.getMessage().contains("depth"));
+        }
+    }
+}
\ No newline at end of file

Reply via email to