This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
The following commit(s) were added to refs/heads/master by this push:
new 0c8f8836 Exclude a few other things by default in DefaultURIResolver
(#150)
0c8f8836 is described below
commit 0c8f883601587a0992c23e9a4fdd83b19dff6371
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Sep 16 11:23:31 2026 +0100
Exclude a few other things by default in DefaultURIResolver (#150)
---
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());
+ }
}