You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ws.apache.org by co...@apache.org on 2026/09/16 10:19:39 UTC
(ws-xmlschema) 01/01: Exclude a few other things by default in DefaultURIResolver
This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch coheigea/uri-resolver
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit e87655fde9a3d3eff95565612b5cf5d03dc5b708
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Wed Sep 16 11:19:00 2026 +0100
Exclude a few other things by default in DefaultURIResolver
---
README.txt | 6 ++-
THREAT-MODEL.md | 22 +++++++--
.../ws/commons/schema/XmlSchemaCollection.java | 4 +-
.../schema/resolver/DefaultURIResolver.java | 51 +++++++++++++-------
.../test/java/tests/DefaultURIResolverTest.java | 56 +++++++++++++++++++++-
5 files changed, 111 insertions(+), 28 deletions(-)
diff --git a/README.txt b/README.txt
index 32e1b930..9c1b8cf5 100644
--- a/README.txt
+++ b/README.txt
@@ -84,8 +84,10 @@ For example, set a limit with:
schema locations through a URIResolver. The bundled DefaultURIResolver
is a convenience for trusted, operator-controlled schema sets. It
resolves http, https, file and jar locations and applies no host or
- address filtering, so a schema location naming an internal host, a
- cloud metadata endpoint, or a local file is fetched on request.
+ address filtering to the http and https targets it allows, so a schema
+ location naming an internal host, a cloud metadata endpoint, or a local
+ file is fetched on request. It does refuse a file: location that names a
+ non-local authority, and a jar: archive fetched over the network.
Applications that parse schema or WSDL documents from an untrusted
source must install a restricting resolver before reading them:
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index b7609303..dffe0103 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -154,7 +154,7 @@ A finding is in-model only if it reaches a row marked **yes**.
| --- | --- | --- | --- |
| B1 | Caller → `XmlSchemaCollection.read(InputSource | Reader | Source | Document | Element)` | none — caller is trusted | none |
| B2 | `XmlSchemaCollection.read(InputSource, ...)` → hardened JDK `DocumentBuilder` | none | external DTD/entity resolution disabled unconditionally; DOCTYPE accepted |
-| B3 | Schema parser → `URIResolver.resolveEntity(namespace, schemaLocation, baseUri)` | none | bundled `DefaultURIResolver` allowlists the effective scheme (`http`, `https`, `file`, `jar`, judged through any `jar:` wrapper) and refuses a location that changes the scheme of a remote base or resolves to a non-local `file:` / `jar:` authority; it does **no host filtering** on the `http(s)` targets it allows |
+| B3 | Schema parser → `URIResolver.resolveEntity(namespace, schemaLocation, baseUri)` | none | bundled `DefaultURIResolver` allowlists the effective scheme (`http`, `https`, `file`, `jar`, judged through any `jar:` wrapper), and unconditionally refuses a `file:` location naming a non-local authority or a `jar:` archive fetched over the network; it also refuses a location that changes the scheme of a remote base; it does **no host filtering** on the `http(s)` targets it allows |
| B4 | Resolved `InputSource` → `XmlSchemaCollection.read(InputSource, ...)` (recursive) | none | none |
| B5 | `XmlSchema.write(...)` → JDK `TransformerFactory` (with `FEATURE_SECURE_PROCESSING=true` and external DTD/stylesheet access disabled where supported) | none | none |
| B6 | `XmlSchemaCollection` ctor → `System.getProperty("org.apache.ws.commons.schema.extension_registry")` → `Class.forName()` | none | trusts system properties to be operator-controlled |
@@ -450,9 +450,13 @@ matching disclaimer.
`InputSource` pointing at it. The JDK then fetches it on parse.
The resolver restricts the *scheme* it will hand back — `http`,
`https`, `file` and `jar`, judged through any `jar:` wrapper — and
- refuses a location that changes the scheme of a remote base or
- resolves to a non-local `file:` / `jar:` authority. Within those
- schemes it applies **no host or address filtering of any kind**: any
+ refuses a location that changes the scheme of a remote base. It also
+ refuses, for every location and whatever the base, a `file:` URL that
+ names a non-local authority (a UNC path on Windows, so an SMB
+ connection to a host the schema author chose) and a `jar:` URL whose
+ archive would be fetched over the network. Within the `http` and
+ `https` targets it does allow, it applies **no host or address
+ filtering of any kind**: any
`http(s)` host is fetched on request, including loopback, link-local
(`169.254.169.254`) and RFC1918 addresses, and the JDK follows HTTP
redirects without consulting the resolver again — so a host allowlist
@@ -689,6 +693,16 @@ Revise this document when any of the following lands:
held. §14 Q12 is ruled (b) in the same pass, which resolves the
conditional dispositions in §5a and §11a to
`BY-DESIGN: property-disclaimed`.
+- **2026-09-16** — a third resolver change, again a revision trigger
+ under the first bullet above: the `file:` and `jar:` authority rules,
+ previously reachable only for *relative* locations composed against a
+ base, now apply to every resolved location, and a `jar:` URL whose
+ archive is fetched over `http`, `https` or `ftp` is refused outright.
+ An absolute `schemaLocation` had been skipping both checks, so
+ `file://host/share/x.xsd` and `jar:http://host/a.jar!/x.xsd` resolved
+ from a local or absent base. §4 B3 and §9 are updated. This does not
+ change the Q12(b) posture: absolute `http(s)` and local `file:`
+ locations are still followed.
- **2026-09-16** — "Fix up DTD handling" (#147) changed the default
parser DTD posture, a revision trigger under the second bullet above:
external DTD and external entity resolution are now disabled
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 fdfdc056..0a367c25 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
@@ -71,8 +71,8 @@ import org.apache.ws.commons.schema.utils.TargetNamespaceValidator;
* <code>schemaLocation</code> is dereferenced through the collection's
* {@link URIResolver}. The default is {@link DefaultURIResolver}, which resolves
* <code>http</code>, <code>https</code>, <code>file</code> and <code>jar</code> locations and
- * applies no host or address filtering. It is a convenience for trusted,
- * operator-controlled schema sets.
+ * applies no host or address filtering to the <code>http</code> and <code>https</code> targets it
+ * allows. It is a convenience for trusted, operator-controlled schema sets.
* </p>
* <p>
* <strong>An application that reads schema or WSDL documents from an untrusted source must
diff --git a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java
index 561a268f..1f7a45f8 100644
--- a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java
+++ b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/resolver/DefaultURIResolver.java
@@ -38,8 +38,9 @@ import org.xml.sax.InputSource;
* <p>
* This resolver is a convenience for trusted, operator-controlled schema sets. It restricts the URI
* schemes it will resolve to <code>http</code>, <code>https</code>, <code>file</code> and
- * <code>jar</code>, and refuses a schema location that changes the scheme of a remote base URI or
- * resolves to a non-local <code>file:</code> / <code>jar:</code> authority. Within those schemes it
+ * <code>jar</code>, and refuses a schema location that changes the scheme of a remote base URI,
+ * names a non-local authority with the <code>file:</code> scheme, or reads a <code>jar:</code>
+ * archive fetched over the network. Within the schemes it does allow it
* applies no host or address filtering, so any reachable host or readable file a schema location
* names is fetched. An application that parses untrusted schema documents must install a restricting
* resolver instead; see
@@ -108,7 +109,7 @@ public class DefaultURIResolver implements CollectionURIResolver {
}
if (isAbsoluteUri(schemaLocation)) {
- verifyAllowedScheme(schemaLocation, schemaLocation);
+ verifyPermittedLocation(schemaLocation, schemaLocation);
return new InputSource(schemaLocation);
}
if (isPlainRelativePath(schemaLocation)) {
@@ -120,7 +121,7 @@ public class DefaultURIResolver implements CollectionURIResolver {
private static void verifyComposedUrl(boolean remoteBase, String originalBaseUri, URL base,
URL composed, String schemaLocation) {
- verifyAllowedScheme(composed.toString(), schemaLocation);
+ verifyPermittedLocation(composed.toString(), schemaLocation);
final String composedScheme = composed.getProtocol().toLowerCase(Locale.ENGLISH);
if (isAbsoluteUri(schemaLocation)) {
if (remoteBase && !isNetworkScheme(composedScheme)) {
@@ -144,33 +145,47 @@ public class DefaultURIResolver implements CollectionURIResolver {
+ "\" changes the scheme of its base URI from \""
+ base.getProtocol() + "\" to \"" + composed.getProtocol() + "\".");
}
- if ("file".equals(composedScheme)) {
- final String host = composed.getHost();
- if (host != null && host.length() > 0 && !"localhost".equalsIgnoreCase(host)) {
- throw new XmlSchemaException("The schema location \"" + schemaLocation
- + "\" resolves to a file URL with a non-local authority.");
- }
- } else if (remoteBase && "jar".equals(composedScheme)
- && composed.toString().regionMatches(true, 0, "jar:file:", 0, 9)
- && !isLocalFileUri(composed.toString().substring(4))) {
- throw new XmlSchemaException("The schema location \"" + schemaLocation
- + "\" resolves to a jar URL with a non-local file authority.");
- }
}
/**
- * Refuse a resolved location whose effective scheme is not one a schema document may use.
+ * Refuse a resolved location this resolver will not dereference. Three rules apply, to every
+ * location and whatever the base URI was: the effective scheme must be one a schema document
+ * may use, a <code>jar:</code> URL may not pull its archive over the network, and a
+ * <code>file:</code> URL may not name a remote authority.
*
* @param uri the resolved location that would be handed to the parser.
* @param schemaLocation the original schema location, for the error message.
*/
- private static void verifyAllowedScheme(String uri, String schemaLocation) {
+ private static void verifyPermittedLocation(String uri, String schemaLocation) {
final String scheme = effectiveScheme(uri);
if (scheme == null || !ALLOWED_SCHEMES.contains(scheme)) {
throw new XmlSchemaException("The schema location \"" + schemaLocation
+ "\" resolves to the scheme \"" + scheme
+ "\", which is not permitted by DefaultURIResolver.");
}
+ final String trimmed = uri.trim();
+ final boolean wrapped = "jar".equals(extractScheme(trimmed));
+ // A jar: URL delegates to the URL of the archive; the entry after "!/" is inside it.
+ final String archive = wrapped ? stripJarEntry(trimmed.substring(4)) : trimmed;
+ if (wrapped && isNetworkScheme(scheme)) {
+ throw new XmlSchemaException("The schema location \"" + schemaLocation
+ + "\" reads an archive fetched over the network, \""
+ + archive + "\", which is not permitted by"
+ + " DefaultURIResolver.");
+ }
+ if ("file".equals(scheme) && !isLocalFileUri(archive)) {
+ throw new XmlSchemaException("The schema location \"" + schemaLocation
+ + "\" resolves to a file URL with a non-local authority.");
+ }
+ }
+
+ /**
+ * Drop the entry part of a jar: URL, leaving the URL of the archive itself. The entry is an
+ * arbitrary path inside the archive and need not parse as part of a URI.
+ */
+ private static String stripJarEntry(String uri) {
+ final int separator = uri.indexOf("!/");
+ return separator < 0 ? uri : uri.substring(0, separator);
}
/**
diff --git a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
index 996fba58..6cadfa04 100644
--- a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
+++ b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
@@ -129,12 +129,18 @@ public class DefaultURIResolverTest extends Assert {
}
private static void assertSchemeRefused(String schemaLocation, String baseUri) {
+ assertSchemeRefused(schemaLocation, baseUri, "not permitted");
+ }
+
+ private static void assertSchemeRefused(String schemaLocation, String baseUri,
+ String expectedMessageFragment) {
DefaultURIResolver resolver = new DefaultURIResolver();
try {
resolver.resolveEntity("urn:x", schemaLocation, baseUri);
- fail("The scheme of \"" + schemaLocation + "\" must be refused.");
+ fail("The location \"" + schemaLocation + "\" must be refused.");
} catch (XmlSchemaException expected) {
- assertTrue(expected.getMessage(), expected.getMessage().contains("not permitted"));
+ assertTrue(expected.getMessage(),
+ expected.getMessage().contains(expectedMessageFragment));
}
}
@@ -182,4 +188,50 @@ public class DefaultURIResolverTest extends Assert {
assertEquals("sub/x.xsd", resolver.resolveEntity("urn:x", "sub/x.xsd", null).getSystemId());
}
+
+ @Test
+ public void testFileUrlWithNonLocalAuthorityIsRefused() {
+ // On Windows this is a UNC path, so the JVM would make an SMB connection to a host the
+ // schema author chose. No base URI makes it reachable: an absolute location used to skip
+ // every check but the scheme allowlist.
+ assertSchemeRefused("file://attacker.example/share/x.xsd", localBase(), "non-local authority");
+ assertSchemeRefused("file://attacker.example/share/x.xsd", null, "non-local authority");
+ assertSchemeRefused("jar:file://attacker.example/share/a.jar!/x.xsd", localBase(),
+ "non-local authority");
+ assertSchemeRefused("jar:file://attacker.example/share/a.jar!/x.xsd", null,
+ "non-local authority");
+ }
+
+ @Test
+ public void testLocalFileAuthoritiesAreStillAccepted() {
+ DefaultURIResolver resolver = new DefaultURIResolver();
+
+ assertEquals("file:///local/x.xsd",
+ resolver.resolveEntity("urn:x", "file:///local/x.xsd", null).getSystemId());
+ assertEquals("file://localhost/local/x.xsd",
+ resolver.resolveEntity("urn:x", "file://localhost/local/x.xsd", null)
+ .getSystemId());
+ }
+
+ @Test
+ public void testJarOverTheNetworkIsRefused() {
+ // JarURLConnection would fetch and cache the whole remote archive, and the URL reads as
+ // protocol "jar" rather than "http".
+ for (String location : new String[] {"jar:http://attacker.example/a.jar!/x.xsd",
+ "jar:https://attacker.example/a.jar!/x.xsd"}) {
+ assertSchemeRefused(location, localBase(), "over the network");
+ assertSchemeRefused(location, null, "over the network");
+ }
+ }
+
+ @Test
+ public void testJarEntryNeedNotParseAsAUri() {
+ // The archive is local; the entry after "!/" is not part of the URI that names it.
+ DefaultURIResolver resolver = new DefaultURIResolver();
+
+ InputSource result = resolver.resolveEntity("urn:x", "jar:file:///tmp/a.jar!/has space.xsd",
+ null);
+
+ assertEquals("jar:file:///tmp/a.jar!/has space.xsd", result.getSystemId());
+ }
}