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());
+    }
 }

Reply via email to