This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/dangling-npe in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit a1aba119a9aef2ce7647f66ecc02585f6d19dd72 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Wed Sep 16 14:58:31 2026 +0100 Fix some potential NPEs --- README.txt | 10 ++ THREAT-MODEL.md | 18 ++-- .../schema/docpath/XmlSchemaPathFinder.java | 5 +- .../ws/commons/schema/walker/XmlSchemaScope.java | 9 ++ .../ws/commons/schema/walker/XmlSchemaWalker.java | 14 ++- .../schema/walker/DanglingReferenceWalkerTest.java | 119 +++++++++++++++++++++ 6 files changed, 165 insertions(+), 10 deletions(-) diff --git a/README.txt b/README.txt index 9c1b8cf5..0cb6324c 100644 --- a/README.txt +++ b/README.txt @@ -68,6 +68,14 @@ adjust the per-document limits: as an entity in one - and FEATURE_SECURE_PROCESSING bounds entity expansion by both count and accumulated size. + The collections returned by the "read-only" accessors on the schema model + are, by default, the live internal collections rather than unmodifiable + views. To wrap them so that modification throws instead, set: + + org.apache.ws.commons.schema.protectReadOnlyCollections + Set to true to return unmodifiable views from the read-only + accessors. The default is false. + For example, set a limit with: -Dorg.apache.ws.commons.schema.walker.maxDecisionPoints=20000 @@ -76,6 +84,8 @@ For example, set a limit with: -Dorg.apache.ws.commons.schema.maxNestingDepth=256 + -Dorg.apache.ws.commons.schema.protectReadOnlyCollections=true + =================== Security =================== diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 24ac6f76..a5fa6dc4 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -243,10 +243,11 @@ A finding is in-model only if it reaches a row marked **yes**. - Opens **no** listening sockets *(inferred — §14 Q10)*. - Spawns **no** child processes *(inferred — §14 Q10)*. - Installs **no** signal handlers *(inferred — §14 Q10)*. -- Reads only the documented system property - `org.apache.ws.commons.schema.extension_registry` for - security-relevant decisions; does **not** consume `LD_*`-style - envvars *(inferred — §14 Q10)*. +- Reads only documented system properties for security-relevant + decisions — `org.apache.ws.commons.schema.extension_registry`, the + resource limits of §5a, and + `org.apache.ws.commons.schema.protectReadOnlyCollections`; does + **not** consume `LD_*`-style envvars *(inferred — §14 Q10)*. - Writes **nothing** to the filesystem of its own initiative; the `XmlSchemaSerializer` writes to the `OutputStream` the caller hands in *(inferred — §14 Q10)*. @@ -269,6 +270,7 @@ points*: | `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.protectReadOnlyCollections` system property | `false` *(documented: `README.txt`, `CollectionFactory.java` lines 37-48)* | in-process convenience, not a trust boundary | when false, the "read-only" model accessors return the **live internal collections**, not unmodifiable views; §7 places the in-process caller outside the attacker model, so this is a correctness guard rather than a security control | | `DocumentBuilderFactory` provider | JDK default (typically Xerces fork) *(inferred — §14 Q6)* | depends on the JDK | shape of XML parsing for `read(InputSource)` / stream-shaped `read(Source)` paths | ### The insecure-default case @@ -802,8 +804,11 @@ untrusted-actor-set value is `OUT-OF-MODEL: trusted-input` (proposed). **Q10.** Negative-side inventory in §5: XMLSchema opens **no** sockets *other than what the JDK URL handler does when following an import*; spawns **no** processes; installs **no** signal handlers; -reads **only** the documented system property; writes **nothing** of -its own initiative. Confirm? *(maps to §5)* +reads **only** the documented system properties (the §5a table, which +includes `protectReadOnlyCollections` — note that its `false` default +means the "read-only" accessors hand out the live internal +collections); writes **nothing** of its own initiative. Confirm? +*(maps to §5)* **Q11.** Build-time variants: confirm there are no compile-time feature toggles; the security envelope is shaped only by runtime extension @@ -915,6 +920,7 @@ the JavaDoc / source comments. The project website is | `XmlSchemaCollection.java` | internal parser sets `FEATURE_SECURE_PROCESSING` and disables external DTD/entity resolution unconditionally; DOCTYPE declarations are accepted | §5a, §8 P2 | | `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 | | `xmlschema-core/src/main/java/.../resolver/DefaultURIResolver.java` | URL composed from `baseUri` + `schemaLocation`; scheme allowlist plus base-scheme / authority checks, but no host filtering | §3 item 7, §9 SSRF disclaim, §10 item 1, §11 first bullet, §14 Q12 | | `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 | 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 8045c13c..e92184e2 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 @@ -28,6 +28,7 @@ import javax.xml.namespace.QName; import org.apache.ws.commons.schema.XmlSchemaAny; import org.apache.ws.commons.schema.XmlSchemaElement; +import org.apache.ws.commons.schema.XmlSchemaException; import org.apache.ws.commons.schema.walker.XmlSchemaTypeInfo; import org.xml.sax.Attributes; import org.xml.sax.SAXException; @@ -449,7 +450,7 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { private void recordDecisionPoint() { ++decisionPointCount; if (decisionPointCount > MAX_DECISION_POINTS) { - throw new IllegalStateException("More than " + MAX_DECISION_POINTS + throw new XmlSchemaException("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" @@ -460,7 +461,7 @@ public final class XmlSchemaPathFinder<U, V> extends DefaultHandler { private void recordReplayedEvent() { ++replayedEventCount; if (replayedEventCount > MAX_REPLAYED_EVENTS) { - throw new IllegalStateException("More than " + MAX_REPLAYED_EVENTS + throw new XmlSchemaException("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" diff --git a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java index fcd7b674..c05349a5 100644 --- a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java +++ b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java @@ -516,6 +516,10 @@ final class XmlSchemaScope { } final QName groupName = groupRef.getTargetQName(); + if (attrGroup == null) { + throw new XmlSchemaException("The attribute group reference " + groupName + + " does not resolve to an attribute group in this collection."); + } if ((groupName != null) && !attributeGroupsInProgress.add(groupName)) { throw new XmlSchemaException("Cyclic attribute group reference detected involving " + groupName + '.'); @@ -586,6 +590,11 @@ final class XmlSchemaScope { } } + if (globalAttr == null) { + throw new XmlSchemaException("The attribute reference " + attrQName + + " does not resolve to an attribute in this collection."); + } + XmlSchemaSimpleType schemaType = globalAttr.getSchemaType(); if (schemaType == null) { final QName typeQName = globalAttr.getSchemaTypeName(); diff --git a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java index 4a906677..b040bbd1 100644 --- a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java +++ b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java @@ -328,8 +328,9 @@ public final class XmlSchemaWalker { } } else if (!element.isAbstract()) { - throw new IllegalStateException("Element " + element.getQName() - + " is not abstract and has no type."); + throw new XmlSchemaException("Element " + element.getQName() + + " is not abstract and has no type; its type reference does" + + " not resolve to a type in this collection."); } // 8. Now handle substitute elements, if any. @@ -369,6 +370,10 @@ public final class XmlSchemaWalker { group = g.getParticle(); } } + if (group == null) { + throw new XmlSchemaException("The group reference " + groupName + + " does not resolve to a group in this collection."); + } walk(group, groupRef.getMinOccurs(), groupRef.getMaxOccurs()); } finally { if (groupName != null) { @@ -567,6 +572,11 @@ public final class XmlSchemaWalker { globalElem = schemasByNamespace.getElementByName(elemQName); } + if (globalElem == null) { + throw new XmlSchemaException("The element reference " + elemQName + + " does not resolve to an element in this collection."); + } + /* * An XML Schema element reference defines the id, minOccurs, and * maxOccurs attributes, while the global element definition defines id diff --git a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/DanglingReferenceWalkerTest.java b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/DanglingReferenceWalkerTest.java new file mode 100644 index 00000000..dfcf4ea6 --- /dev/null +++ b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/DanglingReferenceWalkerTest.java @@ -0,0 +1,119 @@ +/** + * 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.walker; + +import java.io.StringReader; + +import javax.xml.namespace.QName; + +import org.apache.ws.commons.schema.XmlSchemaCollection; +import org.apache.ws.commons.schema.XmlSchemaElement; +import org.apache.ws.commons.schema.XmlSchemaException; + +import org.junit.Assert; +import org.junit.Test; + +/** + * xmlschema-core does not check that references resolve, so a schema carrying a + * dangling ref parses cleanly and only fails when the walker dereferences it. Those + * failures must stay inside the documented XmlSchemaException surface rather than + * escaping as NullPointerException or IllegalStateException. + */ +public class DanglingReferenceWalkerTest extends Assert { + + private static void walkRoot(String body) { + XmlSchemaCollection collection = new XmlSchemaCollection(); + collection.read(new StringReader( + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:tns=\"urn:dangling\"" + + " targetNamespace=\"urn:dangling\" elementFormDefault=\"qualified\">" + + body + + "</xs:schema>")); + XmlSchemaElement root = + collection.getElementByQName(new QName("urn:dangling", "root")); + assertNotNull("the schema under test must declare a 'root' element", root); + new XmlSchemaWalker(collection).walk(root); + } + + private static void assertRejected(String body, String expectedFragment) { + try { + walkRoot(body); + fail("expected the dangling reference to be rejected"); + } catch (XmlSchemaException expected) { + assertTrue(expected.getMessage(), expected.getMessage().contains(expectedFragment)); + } + } + + @Test + public void testDanglingAttributeGroupReferenceIsRejected() { + assertRejected("<xs:complexType name=\"t\">" + + "<xs:attributeGroup ref=\"tns:missing\"/>" + + "</xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:t\"/>", + "attribute group reference"); + } + + @Test + public void testDanglingGroupReferenceIsRejected() { + assertRejected("<xs:complexType name=\"t\"><xs:sequence>" + + "<xs:group ref=\"tns:missing\"/>" + + "</xs:sequence></xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:t\"/>", + "group reference"); + } + + @Test + public void testDanglingElementReferenceIsRejected() { + assertRejected("<xs:complexType name=\"t\"><xs:sequence>" + + "<xs:element ref=\"tns:missing\"/>" + + "</xs:sequence></xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:t\"/>", + "element reference"); + } + + @Test + public void testDanglingAttributeReferenceIsRejected() { + assertRejected("<xs:complexType name=\"t\">" + + "<xs:attribute ref=\"tns:missing\"/>" + + "</xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:t\"/>", + "attribute reference"); + } + + @Test + public void testDanglingTypeReferenceIsRejected() { + assertRejected("<xs:element name=\"root\" type=\"tns:missing\"/>", + "has no type"); + } + + @Test + public void testResolvableReferencesStillWalk() { + walkRoot("<xs:attributeGroup name=\"ag\">" + + "<xs:attribute name=\"a\" type=\"xs:string\"/>" + + "</xs:attributeGroup>" + + "<xs:group name=\"g\"><xs:sequence>" + + "<xs:element name=\"inner\" type=\"xs:string\"/>" + + "</xs:sequence></xs:group>" + + "<xs:element name=\"global\" type=\"xs:string\"/>" + + "<xs:complexType name=\"t\"><xs:sequence>" + + "<xs:group ref=\"tns:g\"/><xs:element ref=\"tns:global\"/>" + + "</xs:sequence><xs:attributeGroup ref=\"tns:ag\"/></xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:t\"/>"); + } +}
