This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/XmlSchemaPathFinder in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 2b7d22cea2062f40fe3f78979ef49a28cfb74b06 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon Aug 24 09:23:07 2026 +0100 Add maxDecisionPoints and maxReplayedEvents to the path walker --- README.txt | 20 ++++++++ THREAT-MODEL.md | 19 +++++++- .../ws/commons/schema/XmlSchemaCollection.java | 5 +- .../schema/docpath/XmlSchemaPathFinder.java | 56 ++++++++++++++++++++++ 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/README.txt b/README.txt index d62a2528..d67f1402 100644 --- a/README.txt +++ b/README.txt @@ -23,6 +23,26 @@ https://ws.apache.org/xmlschema/ Documentation can be found in the 'documents' distribution of this release and in the main site. +=================== + Configuration +=================== + +The XML Schema path finder limits the work it performs while backtracking +through ambiguous schema content models. The following JVM system properties +adjust the per-document limits: + + org.apache.ws.commons.schema.walker.maxDecisionPoints + Maximum decision points created while matching a document. The default is + 10000. + + org.apache.ws.commons.schema.walker.maxReplayedEvents + Maximum previously traversed events replayed while backtracking. The + default is 1000000. + +For example, set a limit with: + + -Dorg.apache.ws.commons.schema.walker.maxDecisionPoints=20000 + =================== Support =================== diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 40ae0ce8..12664adf 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -103,6 +103,7 @@ filesystem IO** when it follows `<xs:include>` / `<xs:import>` / | `xmlschema-core` resource loader (internal) — `DocumentBuilderFactory` and `TransformerFactory` instances configured with `FEATURE_SECURE_PROCESSING=true` | invoked by `XmlSchemaCollection.read(InputSource, ...)` and `XmlSchemaSerializer` | **no** | **yes** | | `xmlschema-walker` visitor — `XmlSchemaWalker`, `XmlSchemaVisitor`, `XmlSchemaScope` | walked by a caller-supplied `XmlSchemaVisitor` | **no** | **yes** | | `xmlschema-walker` element validator — `XmlSchemaElementValidator` | walked at SAX-event time; consults the schema model | **no** (operates on caller-supplied SAX events) | **yes** | +| `xmlschema-walker` document path finder — `XmlSchemaPathFinder` | walked at SAX-event time; matches events against the schema model | **no** (operates on caller-supplied SAX events) | **yes** | | `xmlschema-walker` DOM-from-SAX helper — `DomBuilderFromSax` | builds a DOM out of SAX events for validator inspection | **no** | **yes** | | `xmlschema-bundle-test` | OSGi packaging regression test | n/a | **out of model** *(§3)* | | `w3c-testcases/` | W3C schema test data | n/a | **out of model** *(§3)* | @@ -179,6 +180,10 @@ A finding is in-model only if it reaches a row marked **yes**. schema-model semantics imply about the DOM contents. - **`xmlschema-walker`**: in-model when the visitor walks an attacker-controlled schema. The walker is purely in-memory. +- **`XmlSchemaPathFinder`**: in-model when caller-supplied SAX events + are matched against an attacker-controlled schema. Its backtracking + work is bounded per document by configurable decision-point and + replay-event limits. - **`XmlSchemaElementValidator` / `DomBuilderFromSax`**: in-model only insofar as they parse SAX events the caller hands in. `DomBuilderFromSax` sets `FEATURE_SECURE_PROCESSING=true` on its @@ -243,6 +248,8 @@ points*: | `org.apache.ws.commons.schema.extension_registry` system property | unset *(documented: `XmlSchemaCollection.java` line 361)* | dev-time customization; if set by an untrusted actor the named class is loaded into the JVM | extension-registry class is `Class.forName`-loaded at `XmlSchemaCollection` ctor time | | `XmlSchemaCollection.setSchemaResolver(URIResolver)` | `DefaultURIResolver` *(documented: `DefaultURIResolver.java`)* | **maintainer ruling required** — is the documented expectation that production deployments install a *restricted* resolver that refuses untrusted hosts (proposed: **yes, §10**), or is the default resolver supported as production-safe? *(inferred — §14 Q12)* | controls whether `xs:include`/`xs:import` may reach the network | | `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 | | `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 @@ -282,6 +289,7 @@ feature is the intended defense and is sufficient) or `MODEL-GAP` | `XmlSchemaCollection.setExtReg(ExtensionRegistry)` | registry | caller-supplied | caller's choice | | `XmlSchema.write(OutputStream)` / `XmlSchema.write(Writer)` | output sink | caller-supplied | caller's choice; `FEATURE_SECURE_PROCESSING=true` is set on the internal `TransformerFactory` *(documented: `XmlSchema.java` line 886)* | | `XmlSchemaWalker.walk(XmlSchemaElement)` | walked schema | as untrusted as the schema | none — pure in-memory walking | +| `XmlSchemaPathFinder` | SAX events + schema model | as untrusted as both | configure backtracking limits for the deployment; defaults bound decision points and replayed events per document | | `XmlSchemaElementValidator` (walker module) | SAX events + schema model | as untrusted as both | caller validates / sanitizes outside | | `DomBuilderFromSax` | SAX events | as untrusted as the source of the events | `FEATURE_SECURE_PROCESSING=true` is set *(documented: `DomBuilderFromSax.java` line 81)* | | System property `org.apache.ws.commons.schema.extension_registry` | class name | **trusted by §3 item 7** | operator must lock down property setting in shared-JVM deployments | @@ -293,6 +301,11 @@ feature is the intended defense and is sufficient) or `MODEL-GAP` *(inferred — §14 Q8)*. - No rate limit on URL fetches when following `xs:import` *(inferred — §14 Q12)*. +- `XmlSchemaPathFinder` bounds decision points and replayed events per + document by default; these limits are configurable through the + `org.apache.ws.commons.schema.walker.maxDecisionPoints` and + `org.apache.ws.commons.schema.walker.maxReplayedEvents` system + properties *(documented: `XmlSchemaPathFinder.java`)*. ## §7 Adversary model @@ -579,7 +592,8 @@ Revise this document when any of the following lands: e.g. adding explicit `disallow-doctype-decl=true`. - A new public entry point on `XmlSchemaCollection` that accepts new input shapes. -- A new built-in resource limit on schema size or import-graph depth. +- A new built-in resource limit, or a change to an existing resource + limit. - A new feature flag in `xmlschema-walker`'s validator that turns it into a claimed-conformant document validator. - An upgrade of the supported JDK minimum that changes the JDK XML @@ -601,7 +615,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 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 or import-graph resource 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 | @@ -766,3 +780,4 @@ source comments. The project website is | `xmlschema-core/src/main/java/.../resolver/DefaultURIResolver.java` | URL composed from `baseUri` + `schemaLocation`; no filtering | §3 item 7, §9 SSRF disclaim, §10 item 1, §11 first bullet | | `xmlschema-core/src/main/java/.../resolver/URIResolver.java` | Resolver interface — caller-pluggable | §2 caller-roles, §10 item 1 | | `xmlschema-walker/src/main/java/.../docpath/DomBuilderFromSax.java` line 81 | `factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, TRUE)` | §5a, §8 P2 | +| `xmlschema-walker/src/main/java/.../docpath/XmlSchemaPathFinder.java` | Configurable per-document limits on decision points and replayed events | §4, §5a, §6, §12 | 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 33b0ed8c..ba78332e 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 @@ -28,6 +28,7 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.ArrayDeque; +import java.util.Arrays; import java.util.Collection; import java.util.Deque; import java.util.HashMap; @@ -449,9 +450,7 @@ public final class XmlSchemaCollection { restriction.setBaseType((XmlSchemaSimpleType)xsd.getTypeByName(parent)); if (facets != null) { - for (XmlSchemaFacet facet : facets) { - restriction.getFacets().add(facet); - } + restriction.getFacets().addAll(Arrays.asList(facets)); } simple.setContent(restriction); diff --git a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java index ce138807..8045c13c 100644 --- a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java +++ b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java @@ -58,6 +58,19 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { */ private static final int MAX_DEPTH = 256; + /* + * The backtracking search creates a DecisionPoint per ambiguous element + * and replays traversed elements on every backtrack; without a budget an + * ambiguous (Unique Particle Attribution violating) schema, which this + * library does not reject, drives the search through an exponential + * cross-product of choices. Both bounds are configurable via system + * properties. + */ + private static final int MAX_DECISION_POINTS = + getIntProperty("org.apache.ws.commons.schema.walker.maxDecisionPoints", 10000); + private static final long MAX_REPLAYED_EVENTS = + getIntProperty("org.apache.ws.commons.schema.walker.maxReplayedEvents", 1000000); + private final XmlSchemaNamespaceContext nsContext; private XmlSchemaPathNode<U, V> rootPathNode; @@ -66,6 +79,8 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { private ArrayList<TraversedElement> traversedElements; private ArrayList<DecisionPoint<U, V>> decisionPoints; + private int decisionPointCount; + private long replayedEventCount; private ArrayList<QName> elementStack; private ArrayList<QName> anyStack; @@ -431,6 +446,40 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { decisionPoints = null; // Hopefully there won't be any! } + private void recordDecisionPoint() { + ++decisionPointCount; + if (decisionPointCount > MAX_DECISION_POINTS) { + throw new IllegalStateException("More than " + MAX_DECISION_POINTS + + " decision points were created while matching this document; the schema" + + " likely contains ambiguous (Unique Particle Attribution violating)" + + " content models. The limit may be changed with the" + + " org.apache.ws.commons.schema.walker.maxDecisionPoints system property."); + } + } + + private void recordReplayedEvent() { + ++replayedEventCount; + if (replayedEventCount > MAX_REPLAYED_EVENTS) { + throw new IllegalStateException("More than " + MAX_REPLAYED_EVENTS + + " traversed elements were replayed while backtracking through this" + + " document; the schema likely contains ambiguous (Unique Particle" + + " Attribution violating) content models. The limit may be changed with the" + + " org.apache.ws.commons.schema.walker.maxReplayedEvents system property."); + } + } + + private static int getIntProperty(String name, int defaultValue) { + try { + Integer value = Integer.getInteger(name); + if (value != null) { + return value.intValue(); + } + } catch (SecurityException e) { + // fall through to the default + } + return defaultValue; + } + /** * Kick-starts a new SAX walk, building new <code>XmlSchemaPathNode</code> * and <code>XmlSchemaDocumentNode</code> traversals in the process. @@ -444,6 +493,9 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { traversedElements.clear(); elementStack.clear(); + decisionPointCount = 0; + replayedEventCount = 0; + if (decisionPoints != null) { decisionPoints.clear(); } @@ -523,6 +575,7 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { if (decisionPoints == null) { decisionPoints = new ArrayList<DecisionPoint<U, V>>(4); } + recordDecisionPoint(); decisionPoints.add(decisionPoint); nextPath = decisionPoint.tryNextPath(); @@ -587,6 +640,7 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { int index = priorPoint.traversedElementIndex + 1; for (; index < traversedElements.size(); ++index) { + recordReplayedEvent(); nextPath = null; final TraversedElement te = traversedElements.get(index); @@ -601,6 +655,7 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { final DecisionPoint<U, V> decisionPoint = new DecisionPoint<U, V>(currentPath, possiblePaths, index, elementStack, anyStack); + recordDecisionPoint(); decisionPoints.add(decisionPoint); nextPath = decisionPoint.tryNextPath(); @@ -691,6 +746,7 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { new DecisionPoint<U, V>(currentPath, possiblePaths, traversedElements.size(), elementStack, anyStack); + recordDecisionPoint(); decisionPoints.add(decisionPoint); nextPath = decisionPoint.tryNextPath(); } else {
