This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch coheigea/defaulturiresolver-protocols
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git

commit c1203e38fa01f32c29d02f2411fe09da8cf1bbd6
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Sep 16 10:13:32 2026 +0100

    Restrict default protocols allowed by the DefaultURIResolver
---
 .../schema/resolver/DefaultURIResolver.java        | 51 ++++++++++++++++++-
 .../test/java/tests/DefaultURIResolverTest.java    | 59 ++++++++++++++++++++++
 2 files changed, 109 insertions(+), 1 deletion(-)

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 4082ccc8..cc6e8bb3 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
@@ -23,7 +23,11 @@ import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.Locale;
+import java.util.Set;
 
 import org.apache.ws.commons.schema.XmlSchemaException;
 import org.xml.sax.InputSource;
@@ -34,6 +38,14 @@ import org.xml.sax.InputSource;
  */
 public class DefaultURIResolver implements CollectionURIResolver {
 
+    /**
+     * The URI schemes this resolver is willing to hand back to the parser. 
Schemes outside this
+     * set are never used by schema documents, so refusing them costs nothing 
and keeps the JDK
+     * URL handlers for them out of reach of an untrusted 
<code>schemaLocation</code>.
+     */
+    private static final Set<String> ALLOWED_SCHEMES = 
Collections.unmodifiableSet(
+        new HashSet<String>(Arrays.asList("http", "https", "file", "jar")));
+
     private String collectionBaseURI;
 
     /**
@@ -81,7 +93,11 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
             }
 
         }
-        if (isAbsoluteUri(schemaLocation) || 
isPlainRelativePath(schemaLocation)) {
+        if (isAbsoluteUri(schemaLocation)) {
+            verifyAllowedScheme(schemaLocation, schemaLocation);
+            return new InputSource(schemaLocation);
+        }
+        if (isPlainRelativePath(schemaLocation)) {
             return new InputSource(schemaLocation);
         }
         return null;
@@ -90,6 +106,7 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
 
     private static void verifyComposedUrl(boolean remoteBase, String 
originalBaseUri, URL base,
                                           URL composed, String schemaLocation) 
{
+        verifyAllowedScheme(composed.toString(), schemaLocation);
         final String composedScheme = 
composed.getProtocol().toLowerCase(Locale.ENGLISH);
         if (isAbsoluteUri(schemaLocation)) {
             if (remoteBase && !isNetworkScheme(composedScheme)) {
@@ -127,6 +144,38 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
         }
     }
 
+    /**
+     * Refuse a resolved location whose effective scheme is not one a schema 
document may use.
+     *
+     * @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) 
{
+        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.");
+        }
+    }
+
+    /**
+     * The scheme that is actually dereferenced when the location is fetched. 
A "jar:" URL
+     * delegates to the URL it wraps, so "jar:http://host/a.jar!/x.xsd"; 
performs an HTTP fetch
+     * even though its protocol reads as "jar".
+     */
+    private static String effectiveScheme(String uri) {
+        final String trimmed = uri.trim();
+        final String scheme = extractScheme(trimmed);
+        if ("jar".equals(scheme)) {
+            final String nested = extractScheme(trimmed.substring(4));
+            if (nested != null) {
+                return nested;
+            }
+        }
+        return scheme;
+    }
+
     private static boolean isAbsoluteUri(String uri) {
         if (isWindowsDriveRootedPath(uri)) {
             return false;
diff --git a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java 
b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
index 153a23ee..996fba58 100644
--- a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
+++ b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
@@ -123,4 +123,63 @@ public class DefaultURIResolverTest extends Assert {
         assertTrue(result.getSystemId().startsWith("file:"));
         assertTrue(result.getSystemId().endsWith("imported.xsd"));
     }
+
+    private static String localBase() {
+        return new File(existingDirectory(), "base.xsd").toURI().toString();
+    }
+
+    private static void assertSchemeRefused(String schemaLocation, String 
baseUri) {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+        try {
+            resolver.resolveEntity("urn:x", schemaLocation, baseUri);
+            fail("The scheme of \"" + schemaLocation + "\" must be refused.");
+        } catch (XmlSchemaException expected) {
+            assertTrue(expected.getMessage(), 
expected.getMessage().contains("not permitted"));
+        }
+    }
+
+    @Test
+    public void testDisallowedSchemesAreRefusedFromALocalBase() {
+        assertSchemeRefused("mailto:[email protected]";, localBase());
+        assertSchemeRefused("jrt:/java.base/java/lang/Object.class", 
localBase());
+        assertSchemeRefused("ftp://attacker.example/x.xsd";, localBase());
+    }
+
+    @Test
+    public void testDisallowedSchemesAreRefusedWithoutABase() {
+        assertSchemeRefused("mailto:[email protected]";, null);
+        assertSchemeRefused("jrt:/java.base/java/lang/Object.class", null);
+        assertSchemeRefused("ftp://attacker.example/x.xsd";, null);
+    }
+
+    @Test
+    public void testJarWrappingANetworkUrlIsRefusedAsThatNetworkScheme() {
+        // "jar:ftp://..."; reads as protocol "jar" but performs an FTP fetch, 
so the scheme
+        // check has to look through the jar: wrapper.
+        assertSchemeRefused("jar:ftp://attacker.example/a.jar!/x.xsd";, 
localBase());
+        assertSchemeRefused("jar:ftp://attacker.example/a.jar!/x.xsd";, null);
+    }
+
+    @Test
+    public void testAllowedSchemesStillResolve() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+
+        assertEquals("http://example.com/x.xsd";,
+                     resolver.resolveEntity("urn:x", 
"http://example.com/x.xsd";, localBase())
+                         .getSystemId());
+        assertEquals("https://example.com/x.xsd";,
+                     resolver.resolveEntity("urn:x", 
"https://example.com/x.xsd";, null)
+                         .getSystemId());
+        assertTrue(resolver.resolveEntity("urn:x", "sibling.xsd", localBase())
+                       .getSystemId().startsWith("file:"));
+        assertTrue(resolver.resolveEntity("urn:x", 
"jar:file:///tmp/a.jar!/x.xsd", localBase())
+                       .getSystemId().startsWith("jar:file:"));
+    }
+
+    @Test
+    public void testPlainRelativeLocationWithoutBaseIsNotSchemeChecked() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+
+        assertEquals("sub/x.xsd", resolver.resolveEntity("urn:x", "sub/x.xsd", 
null).getSystemId());
+    }
 }

Reply via email to