This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/domsource in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit ac4381bb0c12f64111ec002f9240ba1a5e04ecb3 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Fri Aug 28 09:18:53 2026 +0100 Fix DOMSource correctness bug --- THREAT-MODEL.md | 23 +++--- .../ws/commons/schema/XmlSchemaCollection.java | 15 +++- .../src/test/java/tests/ReadDomSourceTest.java | 93 ++++++++++++++++++++++ 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md index 4f242b88..39177fcb 100644 --- a/THREAT-MODEL.md +++ b/THREAT-MODEL.md @@ -149,7 +149,7 @@ A finding is in-model only if it reaches a row marked **yes**. | # | Transition | Authentication | Authorization | | --- | --- | --- | --- | -| B1 | Caller → `XmlSchemaCollection.read(InputSource | InputStream | Reader | URL | Document | Element)` | none — caller is trusted | none | +| B1 | Caller → `XmlSchemaCollection.read(InputSource | Reader | Source | Document | Element)` | none — caller is trusted | none | | B2 | `XmlSchemaCollection.read(InputSource, ...)` → hardened JDK `DocumentBuilder` | none | DOCTYPE rejected by default; external DTD/entity resolution disabled | | B3 | Schema parser → `URIResolver.resolveEntity(namespace, schemaLocation, baseUri)` | none | bundled `DefaultURIResolver` does **no host filtering**: it constructs `new URL(new URL(baseUri), schemaLocation)` and hands back an `InputSource` pointing at it | | B4 | Resolved `InputSource` → `XmlSchemaCollection.read(InputSource, ...)` (recursive) | none | none | @@ -159,7 +159,8 @@ A finding is in-model only if it reaches a row marked **yes**. ### Reachability preconditions per family - **`xmlschema-core` parser** (`XmlSchemaCollection.read(InputSource)`, - `.read(InputStream)`, `.read(Reader)`): in-model when the bytes are + `.read(Reader)`, `.read(Source)` for non-`DOMSource` sources): in-model + when the bytes are attacker-controllable. XMLSchema sets `FEATURE_SECURE_PROCESSING=true` on its internal `DocumentBuilderFactory`, rejects DOCTYPE declarations by default, disables external general entities, external parameter @@ -265,7 +266,7 @@ points*: | `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.allowDTD` system property | `false` *(documented: `README.txt`)* | compatibility toggle for operator-controlled deployments | allows DOCTYPE declarations in schema documents; external DTD and external entity resolution remain disabled | -| `DocumentBuilderFactory` provider | JDK default (typically Xerces fork) *(inferred — §14 Q6)* | depends on the JDK | shape of XML parsing for `read(InputSource)` / `read(InputStream)` paths | +| `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 @@ -279,7 +280,8 @@ required to install a restricting resolver per §10). XMLSchema's internal schema parser rejects DOCTYPE declarations by default and disables external DTD and external entity resolution. This -applies both to top-level `read(InputSource | InputStream | Reader)` +applies both to top-level `read(InputSource | Reader | Source)` non-DOMSource +paths parses and to recursive import/include/redefine reparses. Operators may set `org.apache.ws.commons.schema.allowDTD=true` to accept DOCTYPE-bearing schemas for compatibility; external DTD and external @@ -292,9 +294,8 @@ entity resolution remain disabled in that mode. | Entry point | Parameter | Attacker-controllable? | Caller must enforce | | --- | --- | --- | --- | | `XmlSchemaCollection.read(InputSource is)` | `is` bytes | **yes** | XMLSchema rejects DOCTYPE by default and disables external DTD/entity resolution; caller may need to install a restricting `URIResolver` if the source contains untrusted `xs:include`/`xs:import` | -| `XmlSchemaCollection.read(InputStream in)` | `in` bytes | **yes** | same as above | | `XmlSchemaCollection.read(Reader r)` | `r` characters | **yes** | same as above | -| `XmlSchemaCollection.read(URL url)` | `url` | caller-supplied | caller controls; XMLSchema fetches via JDK URL handlers | +| `XmlSchemaCollection.read(Source src)` | `src` | **yes** | `SAXSource`/`StreamSource`/other route through the internal hardened factory (same as `read(InputSource)`); a `DOMSource` routes to the pre-parsed `read(Document)`/`read(Element)` path, so the upstream parser's XXE/DTD posture applies | | `XmlSchemaCollection.read(Document doc)` | `doc` | **yes if doc was parsed from untrusted bytes** | caller's `DocumentBuilderFactory` is responsible for XXE / DTD posture; XMLSchema does not re-parse | | `XmlSchemaCollection.read(Element el)` | `el` | same as `read(Document)` | same as above | | `XmlSchemaCollection.setSchemaResolver(URIResolver)` | resolver | caller-supplied | replacing the default is the documented path for production hardening *(inferred — §14 Q12)* | @@ -368,8 +369,8 @@ entity resolution remain disabled in that mode. ### P2 — Internal parser DTD and external-entity hardening -- **Condition**: XMLSchema's `read(InputSource | InputStream | Reader)` - paths take the internal `DocumentBuilderFactory`. +- **Condition**: XMLSchema's `read(InputSource | Reader | Source)` + non-DOMSource paths take the internal `DocumentBuilderFactory`. - **Property**: DOCTYPE declarations are rejected by default. External general entities, external parameter entities, external DTD loading, and JAXP external-DTD access are disabled; a no-op `EntityResolver` is @@ -625,8 +626,10 @@ model, the section that licenses the call. top-level `w3c-testcases/`)*. → `OUT-OF-MODEL: unsupported-component`. - **"`ExtensionRegistry` is `Class.forName`-loaded — RCE."** Trusted system property per §3 item 7. → `OUT-OF-MODEL: trusted-input`. -- **"`XmlSchemaCollection.read(URL)` follows `file://` to read - arbitrary local files."** Caller passed the URL; trusted entry point. +- **"`XmlSchemaCollection.read(InputSource)` with a `file://` system ID + follows it to read arbitrary local files."** (No `read(URL)` or + `read(InputStream)` overload exists; earlier revisions of this table + listed both in error.) Caller passed the URL; trusted entry point. → `OUT-OF-MODEL: trusted-input`. ## §12 Conditions that would change this model 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 d20c08bb..8eb23785 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 @@ -633,6 +633,12 @@ public final class XmlSchemaCollection { /** * Read an XML schema into the collection from a TRaX source. Schemas in a collection must be unique in * the concatenation of system ID and targetNamespace. In this API, the systemID is taken from the Source. + * <p> + * Note: a {@link DOMSource} is read through the pre-parsed {@link #read(Document)} / + * {@link #read(Element)} path, so the XXE/DTD posture of that parse is whatever the upstream + * parser that produced the DOM was configured with — this collection's internally hardened + * parser is not involved. All other source types are parsed with the internal hardened parser. + * </p> * * @param source the XSD document. * @return the XML schema object. @@ -642,10 +648,15 @@ public final class XmlSchemaCollection { return read(((SAXSource)source).getInputSource()); } else if (source instanceof DOMSource) { Node node = ((DOMSource)source).getNode(); + String systemId = source.getSystemId(); if (node instanceof Document) { - node = ((Document)node).getDocumentElement(); + return read((Document)node, systemId); + } else if (node instanceof Element) { + return read((Element)node, systemId); } - return read((Document)node); + throw new XmlSchemaException("A DOMSource must wrap a Document or an Element node, but " + + (node == null ? "no node was provided." + : "a " + node.getClass().getName() + " was provided.")); } else if (source instanceof StreamSource) { StreamSource ss = (StreamSource)source; InputSource isource = new InputSource(ss.getSystemId()); diff --git a/xmlschema-core/src/test/java/tests/ReadDomSourceTest.java b/xmlschema-core/src/test/java/tests/ReadDomSourceTest.java new file mode 100644 index 00000000..b14cb60a --- /dev/null +++ b/xmlschema-core/src/test/java/tests/ReadDomSourceTest.java @@ -0,0 +1,93 @@ +/** + * 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 tests; + +import java.io.StringReader; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.dom.DOMSource; + +import org.apache.ws.commons.schema.XmlSchema; +import org.apache.ws.commons.schema.XmlSchemaCollection; +import org.apache.ws.commons.schema.XmlSchemaException; + +import org.junit.Assert; +import org.junit.Test; + +import org.w3c.dom.Document; + +import org.xml.sax.InputSource; + +/** + * read(Source) with a DOMSource must route to the pre-parsed read paths + * (previously it threw ClassCastException for every DOMSource), and must + * reject DOMSources wrapping anything but a Document or Element. + */ +public class ReadDomSourceTest extends Assert { + + private static final String SCHEMA = + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + + " targetNamespace=\"urn:domsource\">" + + "<xs:element name=\"e\" type=\"xs:string\"/>" + + "</xs:schema>"; + + private Document parse() throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + return factory.newDocumentBuilder().parse(new InputSource(new StringReader(SCHEMA))); + } + + @Test + public void testDomSourceWrappingDocument() throws Exception { + XmlSchemaCollection collection = new XmlSchemaCollection(); + XmlSchema schema = collection.read(new DOMSource(parse())); + assertNotNull(schema); + assertEquals("urn:domsource", schema.getTargetNamespace()); + } + + @Test + public void testDomSourceWrappingElement() throws Exception { + XmlSchemaCollection collection = new XmlSchemaCollection(); + XmlSchema schema = collection.read(new DOMSource(parse().getDocumentElement())); + assertNotNull(schema); + assertEquals("urn:domsource", schema.getTargetNamespace()); + } + + @Test + public void testDomSourceSystemIdIsPreserved() throws Exception { + XmlSchemaCollection collection = new XmlSchemaCollection(); + DOMSource source = new DOMSource(parse()); + source.setSystemId("urn:domsource:system-id"); + XmlSchema schema = collection.read(source); + assertEquals("urn:domsource:system-id", schema.getSourceURI()); + } + + @Test + public void testDomSourceWrappingOtherNodeIsRejected() throws Exception { + XmlSchemaCollection collection = new XmlSchemaCollection(); + Document doc = parse(); + try { + collection.read(new DOMSource(doc.createTextNode("not a schema"))); + fail("A DOMSource wrapping a text node should be rejected."); + } catch (XmlSchemaException expected) { + // documented failure surface (previously ClassCastException) + } + } +} \ No newline at end of file
