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
