This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/element-depth in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit de2375f4cc80b450ad12f8ae77cb2e6932275741 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Sep 25 09:30:51 2026 +0100 Misc fixes --- 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));
