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:38 UTC
(ws-xmlschema) branch coheigea/uri-resolver created (now e87655fd)
This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a change to branch coheigea/uri-resolver
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
at e87655fd Exclude a few other things by default in DefaultURIResolver
This branch includes the following new commits:
new e87655fd Exclude a few other things by default in DefaultURIResolver
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(ws-xmlschema) 01/01: Exclude a few other things by default in DefaultURIResolver
Posted by co...@apache.org.
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());
+ }
}