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 681897ca Misc fixes (#180)
681897ca is described below
commit 681897ca955b649e545456b0e26e02e07cab86fa
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Fri Sep 25 09:37:23 2026 +0100
Misc fixes (#180)
---
README.txt | 11 +++++
SECURITY.md | 10 ++++
THREAT-MODEL.md | 31 +++++++++---
.../ws/commons/schema/XmlSchemaCollection.java | 57 +++++++++++++---------
.../ws/commons/schema/ElementDepthLimitTest.java | 43 ++++++++++++++++
.../src/test/java/tests/NestingDepthLimitTest.java | 15 ++++++
6 files changed, 138 insertions(+), 29 deletions(-)
diff --git a/README.txt b/README.txt
index 6d11ef45..f075a5ff 100644
--- a/README.txt
+++ b/README.txt
@@ -79,6 +79,17 @@ stack. The following JVM system property adjusts the limit:
as an entity in one - and FEATURE_SECURE_PROCESSING bounds entity
expansion by both count and accumulated size.
+ The same parser also bounds the element depth of the document, including
+ markup an entity expands to, so deep markup is refused before it can
+ exhaust the thread stack. The limit is set by maxNestingDepth above:
+ twice its value plus 64, which is 1088 by default, since the schema build
+ never accepts a document deeper than that. A lower jdk.xml.maxElementDepth,
+ is kept, except that on JDK 8, 11 and 17 only a system property is seen:
+ a lower limit set just in the JDK's jaxp.properties file is raised to
+ this one there, so set -Djdk.xml.maxElementDepth instead. Raising
+ jdk.xml.maxElementDepth alone has no effect; raise maxNestingDepth
+ instead.
+
When the DefaultURIResolver fetches a schema over http or https, the fetch
is bounded: left to the JDK it has no timeout and no size limit, so one
schemaLocation naming a slow or endless host can hold a parsing thread or
diff --git a/SECURITY.md b/SECURITY.md
index 80b5d4fb..f38b309c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -56,6 +56,16 @@ source must install a restricting resolver via
See [THREAT-MODEL.md](./THREAT-MODEL.md) section 10 for the full set of
downstream responsibilities.
+The parser `XmlSchemaCollection` reads schema documents with never
+resolves external DTDs or external entities, and bounds the element depth
+of what it parses, including markup that an entity expands to. That depth
+limit is set by `org.apache.ws.commons.schema.maxNestingDepth`: twice its
+value plus 64, which is 1088 by default. A lower `jdk.xml.maxElementDepth`
+is kept (on JDK 8, 11 and 17, only when set as a system property rather
+than in `jaxp.properties`), but raising `jdk.xml.maxElementDepth` alone has
+no effect. The
+library's resource limits and how to set them are listed in README.txt.
+
A report that the bundled default resolver dereferenced an attacker-supplied
`schemaLocation` is a documented property of that default, not a
vulnerability in the library (THREAT-MODEL.md section 9 and section 14
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index 7a155f9b..f6acfa06 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -170,7 +170,9 @@ A finding is in-model only if it reaches a row marked
**yes**.
installs a no-op SAX `EntityResolver`. A DOCTYPE declaration is
accepted: an internal DTD subset carries no external reference, and
entity expansion stays bounded by `FEATURE_SECURE_PROCESSING` on both
- count and accumulated size. There is no property to relax the
+ count and accumulated size. The parse is bounded in element depth too,
+ expanded entity markup included, at twice `maxNestingDepth` plus 64
+ (§5a, §8 P6). There is no property to relax the
external-resolution controls, and none to tighten the DOCTYPE posture.
- **`xmlschema-core` URI resolver** (`DefaultURIResolver`): in-model
for SSRF / cross-origin fetch when the input schema is attacker-
@@ -289,7 +291,7 @@ points*:
| `org.apache.ws.commons.schema.walker.maxDepth` system property | `256`
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum
depth of nested elements, model groups and substitution group members while
walking a schema, and separately of type derivation and of attribute group
references; a deployment running the walker on threads with small stacks (under
about 512 KB) should lower it |
| `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 |
-| `org.apache.ws.commons.schema.maxNestingDepth` system property | `512`
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum
structural nesting depth while building the schema model, including nested
include/import/redefine document resolutions |
+| `org.apache.ws.commons.schema.maxNestingDepth` system property | `512`
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum
structural nesting depth while building the schema model, including nested
include/import/redefine document resolutions. It also sets the internal
parser's element-depth limit, `2 × maxNestingDepth + 64` (`1088` by default,
and never below `1`); a lower `jdk.xml.maxElementDepth` is kept (on JDK 8, 11
and 17 only when set as a system property [...]
| `org.apache.ws.commons.schema.remote.allowNetwork` system property | `true`
*(documented: `README.txt`)* | operator opt-out for deployments with no remote
schema sets | when `false`, `DefaultURIResolver` refuses a location whose
effective scheme is `http` or `https`; local `file:` / `jar:` reads are
unaffected, so it closes the remote-fetch half of §9's SSRF disclaimer but not
the local-read half |
| `org.apache.ws.commons.schema.remote.checkAddresses` system property |
`true` *(documented: `README.txt`)* | shipped default: refuse address classes
that never serve a schema | before a remote fetch, and again on each redirect
hop, every address the host resolves to is checked; link-local
(`169.254.169.254`), multicast, wildcard, IPv6 unique-local (`fd00::/7`) and
IPv6 forms embedding one are refused. Loopback and RFC 1918 are permitted.
Skipped when a proxy would carry the fetch, sinc [...]
| `org.apache.ws.commons.schema.remote.maxRedirects` system property | `5`
*(documented: `README.txt`)* | operator-tunable bound on one fetch's redirect
chain | `DefaultURIResolver` follows redirects itself rather than leaving them
to the JDK, so the chain is bounded, each hop is re-checked against the scheme
and authority rules, and the chain shares one fetch deadline; `0` refuses a
redirected location. A hop that changes scheme is refused |
@@ -465,9 +467,14 @@ leave open.
to `XmlSchemaCollection.read(...)`.
- **Property**: deeply nested schema structure, including depth split across
nested include/import/redefine document resolutions, terminates with
- `XmlSchemaException` instead of exhausting the Java thread stack.
-- **Violation symptom**: a parseable schema causes `SchemaBuilder` to recurse
- until `StackOverflowError` or another resource-exhaustion failure.
+ `XmlSchemaException` instead of exhausting the Java thread stack. Before
+ the build starts, the internal parser refuses markup deeper than twice
+ `maxNestingDepth` plus 64, the most any schema the build accepts can
+ need, so markup an entity in the internal DTD subset expands to cannot
+ exhaust the stack inside the JDK parser either.
+- **Violation symptom**: a parseable schema causes `SchemaBuilder` or the
+ internal parser to recurse until `StackOverflowError` or another
+ resource-exhaustion failure.
- **Severity**: **medium** availability impact when the embedding
application accepts untrusted schemas.
@@ -766,6 +773,18 @@ Revise this document when any of the following lands:
rule as first written: it tested only the URI authority, so
`file:////host/share/x.xsd`, which parses with no authority and
carries the host in its path instead, was not caught.
+- **2026-09-25** — a new resource limit, a revision trigger under the
+ fourth bullet above: the internal parser now bounds the element depth of
+ the document it parses, at twice `maxNestingDepth` plus 64 (#179). A
+ 35 KB schema whose internal-subset entity expanded to markup a few
+ thousand elements deep overflowed the stack inside the JDK parser, before
+ `SchemaBuilder`'s own bound could see it. The limit is derived from
+ `maxNestingDepth` rather than given its own property, and never falls
+ below `1`. A lower `jdk.xml.maxElementDepth` is kept where the library
+ can see it: always on JDK 21 and later, and only as a system property on
+ JDK 8, 11 and 17, which do not report a `jaxp.properties` setting; there
+ such a setting is raised to this limit, which is still safe on ordinary
+ thread stacks. §4 reachability, §5a and §8 P6 are updated.
- **2026-09-24** — `DefaultURIResolver` now refuses a local location that
exists but is not a regular file. A remote fetch is bounded in time and
bytes by the properties in §5a; a local read is handed to the parser as a
@@ -1042,7 +1061,7 @@ the JavaDoc / source comments. The project website is
| `RELEASE-NOTE.txt` (2.3.0) | Java 17 minimum, Java 7 dropped | §5
environment |
|
`xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaCollection.java`
line 361 | `org.apache.ws.commons.schema.extension_registry` system property
loaded via `Class.forName` | §5a, §6, §11 |
|
`xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java`
| `org.apache.ws.commons.schema.maxNestingDepth` structural descent limit | §5,
§5a, §6, §8 P6 |
-| `XmlSchemaCollection.java` | internal parser sets
`FEATURE_SECURE_PROCESSING` and disables external DTD/entity resolution
unconditionally; DOCTYPE declarations are accepted | §5a, §8 P2 |
+| `XmlSchemaCollection.java` | internal parser sets
`FEATURE_SECURE_PROCESSING` and disables external DTD/entity resolution
unconditionally; DOCTYPE declarations are accepted; element depth bounded at `2
× maxNestingDepth + 64` | §5a, §8 P2, §8 P6 |
| `XmlSchemaCollection.java` line 745 | `AccessController.doPrivileged`
wrapper for the SAX parse | §5 |
| `XmlSchema.java` | serializer `TransformerFactory` sets
`FEATURE_SECURE_PROCESSING` and disables external DTD/stylesheet access where
supported | §5a, §8 P2 |
| `XmlSchemaSerializer.java` lines 1566-1567 | `DocumentBuilderFactory` sets
`FEATURE_SECURE_PROCESSING`; used only via `newDocument()`, so it never parses
input and carries no XXE surface | §5a, §8 P2 |
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 32d21c98..7139bd4a 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
@@ -885,7 +885,7 @@ public final class XmlSchemaCollection {
} catch (IOException e) {
throw new XmlSchemaException(e.getMessage(), e);
} catch (SAXException e) {
- if (e.getMessage() != null &&
e.getMessage().contains(ELEMENT_DEPTH_ERROR)) {
+ if (e.getMessage() != null &&
e.getMessage().startsWith(ELEMENT_DEPTH_ERROR + ":")) {
// Name the property that governs the limit: raising
jdk.xml.maxElementDepth alone
// has no effect, as limitElementDepth() lowers it again.
throw new XmlSchemaException("The schema document is nested
too deeply to parse ("
@@ -935,22 +935,6 @@ public final class XmlSchemaCollection {
return doc;
}
- /**
- * Blocks the external-resolution half of DTD processing, which is what
makes
- * DTDs dangerous: external general entities, external parameter entities
and
- * the external DTD subset are never fetched, so a schema document cannot
read
- * local files or reach the network through its DOCTYPE.
- * <p>
- * The DOCTYPE declaration itself is accepted. An internal DTD subset is a
- * legitimate and widely used part of real schema documents - the W3C's own
- * normative schemas (XML Signature, XML Encryption, XKMS) declare the
- * entities they use for their target namespace in one - and refusing it
- * closes no attack path that is still open here: it holds no external
- * reference, and {@code FEATURE_SECURE_PROCESSING} bounds entity expansion
- * by both count and accumulated size, so neither nested nor flat expansion
- * runs away.
- * </p>
- */
/**
* The JDK parser's element-depth limit,
<code>jdk.xml.maxElementDepth</code>, under the name
* JDK 8 onwards accepts on a factory.
@@ -958,7 +942,7 @@ public final class XmlSchemaCollection {
private static final String MAX_ELEMENT_DEPTH =
"http://www.oracle.com/xml/jaxp/properties/maxElementDepth";
- /** The code the JDK parser's message carries when that limit is exceeded.
*/
+ /** The code that starts the JDK parser's message when that limit is
exceeded. */
private static final String ELEMENT_DEPTH_ERROR = "JAXP00010006";
/**
@@ -969,10 +953,10 @@ public final class XmlSchemaCollection {
* deeper than about twice its structural bound, maxNestingDepth, since
each counted level (an
* element, a type, a model group) is at most two XML levels deep, so
refusing deeper markup
* in the parser costs nothing. A lower limit already set, by
jdk.xml.maxElementDepth or by the
- * JDK's own default, is left in place.
+ * JDK's own default, is left in place where it can be seen: see
currentElementDepthLimit.
*/
private static void limitElementDepth(DocumentBuilderFactory docFac) {
- final long limit = Math.min(Integer.MAX_VALUE, 2L *
SchemaBuilder.MAX_NESTING_DEPTH + 64L);
+ final long limit = elementDepthLimit(SchemaBuilder.MAX_NESTING_DEPTH);
final long existing = currentElementDepthLimit(docFac);
if (existing > 0 && existing <= limit) {
return;
@@ -984,11 +968,22 @@ public final class XmlSchemaCollection {
}
}
+ /**
+ * The parser element-depth limit for a given maxNestingDepth: twice it
plus 64, kept between
+ * 1 and Integer.MAX_VALUE so that no setting of maxNestingDepth, however
extreme, turns the
+ * limit off (the JDK reads 0 or less as no limit).
+ */
+ static long elementDepthLimit(int maxNestingDepth) {
+ return Math.max(1L, Math.min(Integer.MAX_VALUE, 2L * maxNestingDepth +
64L));
+ }
+
/**
* The element-depth limit already in force, or 0 for none. Only some JDKs
report it through
- * the factory: JDK 21 does; JDK 8 throws IllegalArgumentException, and
JDK 17 reports only
- * attributes set on the factory itself, returning null or throwing
NullPointerException. When
- * the factory gives no value, the system property the parser takes it
from is read instead.
+ * the factory: JDK 21 does; JDK 8 and 11 throw IllegalArgumentException,
and JDK 17 reports
+ * only attributes set on the factory itself, returning null or throwing
NullPointerException.
+ * When the factory gives no value, the system property the parser takes
it from is read
+ * instead. On those JDKs a limit set only in the JDK's jaxp.properties
file is not seen, and
+ * is raised to this library's own.
*/
private static long currentElementDepthLimit(DocumentBuilderFactory
docFac) {
Object current = null;
@@ -1007,6 +1002,22 @@ public final class XmlSchemaCollection {
return getIntProperty("jdk.xml.maxElementDepth", 0);
}
+ /**
+ * Blocks the external-resolution half of DTD processing, which is what
makes
+ * DTDs dangerous: external general entities, external parameter entities
and
+ * the external DTD subset are never fetched, so a schema document cannot
read
+ * local files or reach the network through its DOCTYPE.
+ * <p>
+ * The DOCTYPE declaration itself is accepted. An internal DTD subset is a
+ * legitimate and widely used part of real schema documents - the W3C's own
+ * normative schemas (XML Signature, XML Encryption, XKMS) declare the
+ * entities they use for their target namespace in one - and refusing it
+ * closes no attack path that is still open here: it holds no external
+ * reference, and {@code FEATURE_SECURE_PROCESSING} bounds entity expansion
+ * by both count and accumulated size, so neither nested nor flat expansion
+ * runs away.
+ * </p>
+ */
private static void hardenAgainstDtdProcessing(DocumentBuilderFactory
docFac) {
trySetFeature(docFac,
"http://xml.org/sax/features/external-general-entities", false);
trySetFeature(docFac,
"http://xml.org/sax/features/external-parameter-entities", false);
diff --git
a/xmlschema-core/src/test/java/org/apache/ws/commons/schema/ElementDepthLimitTest.java
b/xmlschema-core/src/test/java/org/apache/ws/commons/schema/ElementDepthLimitTest.java
new file mode 100644
index 00000000..3564360c
--- /dev/null
+++
b/xmlschema-core/src/test/java/org/apache/ws/commons/schema/ElementDepthLimitTest.java
@@ -0,0 +1,43 @@
+/**
+ * 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 org.apache.ws.commons.schema;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * The parser element-depth limit that XmlSchemaCollection derives from
maxNestingDepth.
+ */
+public class ElementDepthLimitTest extends Assert {
+
+ @Test
+ public void testLimitIsTwiceTheNestingDepthPlus64() {
+ assertEquals(1088L, XmlSchemaCollection.elementDepthLimit(512));
+ }
+
+ @Test
+ public void testExtremeNestingDepthDoesNotDisableTheLimit() {
+ assertEquals(30L, XmlSchemaCollection.elementDepthLimit(-17));
+ assertEquals(1L, XmlSchemaCollection.elementDepthLimit(-32));
+ assertEquals(1L, XmlSchemaCollection.elementDepthLimit(-33));
+ assertEquals(1L,
XmlSchemaCollection.elementDepthLimit(Integer.MIN_VALUE));
+ assertEquals(Integer.MAX_VALUE,
XmlSchemaCollection.elementDepthLimit(Integer.MAX_VALUE));
+ }
+}
diff --git a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
index 68d87ed5..cb4a0a05 100644
--- a/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
+++ b/xmlschema-core/src/test/java/tests/NestingDepthLimitTest.java
@@ -273,6 +273,21 @@ public class NestingDepthLimitTest extends Assert {
}
}
+ /**
+ * Only the parser's own depth error is reworded: a malformed document
whose element is named
+ * after the error code keeps the parser's message.
+ */
+ @Test
+ public void testUnrelatedErrorNamingTheDepthCodeIsNotReworded() throws
Exception {
+ try {
+ new XmlSchemaCollection().read(new StringReader(
+ "<xs:schema
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"><JAXP00010006></xs:schema>"));
+ fail("A malformed schema should be refused.");
+ } catch (XmlSchemaException expected) {
+ assertFalse(expected.getMessage(),
expected.getMessage().contains("nested too deeply"));
+ }
+ }
+
private static void assertRejectedAsNested(String schema) {
try {
new XmlSchemaCollection().read(new StringReader(schema));