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 95e3f643 Add maxDecisionPoints and maxReplayedEvents to the path
walker (#125)
95e3f643 is described below
commit 95e3f6436c3d17f7d82c310d15bef93a740229a2
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Mon Aug 24 09:45:07 2026 +0100
Add maxDecisionPoints and maxReplayedEvents to the path walker (#125)
---
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 {