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

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

commit d2cfa2e83a55d157010f7e110b8ba335323a7dd2
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Aug 27 10:04:38 2026 +0100

    Harden default resolver
---
 .../schema/resolver/DefaultURIResolver.java        | 169 +++++++++++++++++++--
 .../test/java/tests/DefaultURIResolverTest.java    | 126 +++++++++++++++
 2 files changed, 279 insertions(+), 16 deletions(-)

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 79747b66..4082ccc8 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
@@ -21,8 +21,11 @@ package org.apache.ws.commons.schema.resolver;
 import java.io.File;
 import java.net.MalformedURLException;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.Locale;
 
+import org.apache.ws.commons.schema.XmlSchemaException;
 import org.xml.sax.InputSource;
 
 /**
@@ -43,36 +46,170 @@ public class DefaultURIResolver implements 
CollectionURIResolver {
     public InputSource resolveEntity(String namespace, String schemaLocation, 
String baseUri) {
 
         if (baseUri != null) {
+            final String originalBaseUri = baseUri;
+            final boolean remoteBase = isRemoteBase(baseUri);
             try {
-                File baseFile = null;
-                try {
-                    URI uri = new URI(baseUri);
-                    baseFile = new File(uri);
-                    if (!baseFile.exists()) {
+                if (!remoteBase) {
+                    File baseFile = null;
+                    try {
+                        URI uri = new URI(baseUri);
+                        baseFile = new File(uri);
+                        if (!baseFile.exists()) {
+                            baseFile = new File(baseUri);
+                        }
+                    } catch (Throwable ex) {
                         baseFile = new File(baseUri);
                     }
-                } catch (Throwable ex) {
-                    baseFile = new File(baseUri);
-                }
-                if (baseFile.exists()) {
-                    baseUri = baseFile.toURI().toString();
-                } else if (collectionBaseURI != null) {
-                    baseFile = new File(collectionBaseURI);
                     if (baseFile.exists()) {
                         baseUri = baseFile.toURI().toString();
+                    } else if (collectionBaseURI != null) {
+                        baseFile = new File(collectionBaseURI);
+                        if (baseFile.exists()) {
+                            baseUri = baseFile.toURI().toString();
+                        }
                     }
                 }
 
-                String ref = new URL(new URL(baseUri), 
schemaLocation).toString();
+                URL base = new URL(baseUri);
+                URL ref = new URL(base, schemaLocation);
+                verifyComposedUrl(remoteBase, originalBaseUri, base, ref, 
schemaLocation);
 
-                return new InputSource(ref);
+                return new InputSource(ref.toString());
             } catch (MalformedURLException e1) {
-                throw new RuntimeException(e1);
+                throw new XmlSchemaException("Unable to resolve the schema 
location \"" + schemaLocation
+                                             + "\" against the base URI \"" + 
baseUri + "\"", e1);
             }
 
         }
-        return new InputSource(schemaLocation);
+        if (isAbsoluteUri(schemaLocation) || 
isPlainRelativePath(schemaLocation)) {
+            return new InputSource(schemaLocation);
+        }
+        return null;
+
+    }
+
+    private static void verifyComposedUrl(boolean remoteBase, String 
originalBaseUri, URL base,
+                                          URL composed, String schemaLocation) 
{
+        final String composedScheme = 
composed.getProtocol().toLowerCase(Locale.ENGLISH);
+        if (isAbsoluteUri(schemaLocation)) {
+            if (remoteBase && !isNetworkScheme(composedScheme)) {
+                throw new XmlSchemaException("The schema location \"" + 
schemaLocation
+                                             + "\" of the remote base URI \"" 
+ originalBaseUri
+                                             + "\" uses the non-network scheme 
\"" + composedScheme
+                                             + "\".");
+            }
+            return;
+        }
+        if (remoteBase) {
+            final String originalScheme = extractScheme(originalBaseUri);
+            if (originalScheme != null && 
!originalScheme.equals(composedScheme)) {
+                throw new XmlSchemaException("The schema location \"" + 
schemaLocation
+                                             + "\" changes the scheme of its 
base URI from \""
+                                             + originalScheme + "\" to \"" + 
composedScheme + "\".");
+            }
+        }
+        if (!composed.getProtocol().equalsIgnoreCase(base.getProtocol())) {
+            throw new XmlSchemaException("The schema location \"" + 
schemaLocation
+                                         + "\" 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.");
+        }
+    }
+
+    private static boolean isAbsoluteUri(String uri) {
+        if (isWindowsDriveRootedPath(uri)) {
+            return false;
+        }
+        try {
+            return new URI(uri).isAbsolute();
+        } catch (URISyntaxException e) {
+            return false;
+        }
+    }
+
+    private static boolean isRemoteBase(String uri) {
+        final String trimmed = uri.trim();
+        if (trimmed.startsWith("//")) {
+            return true;
+        }
+        final String scheme = extractScheme(trimmed);
+        if (scheme == null) {
+            return false;
+        }
+        if ("file".equals(scheme)) {
+            return !isLocalFileUri(trimmed);
+        }
+        if ("urn".equals(scheme)) {
+            return false;
+        }
+        if ("jar".equals(scheme)) {
+            final String nested = extractScheme(trimmed.substring(4));
+            return !"file".equals(nested) || 
!isLocalFileUri(trimmed.substring(4));
+        }
+        return true;
+    }
+
+    private static boolean isLocalFileUri(String uri) {
+        try {
+            URI parsed = new URI(uri);
+            final String authority = parsed.getAuthority();
+            return "file".equalsIgnoreCase(parsed.getScheme())
+                && (authority == null || authority.length() == 0 || 
"localhost".equalsIgnoreCase(authority));
+        } catch (URISyntaxException e) {
+            return false;
+        }
+    }
+
+    private static boolean isNetworkScheme(String scheme) {
+        return "http".equals(scheme) || "https".equals(scheme) || 
"ftp".equals(scheme);
+    }
+
+    private static String extractScheme(String uri) {
+        final String trimmed = uri.trim();
+        final int colon = trimmed.indexOf(':');
+        if (colon <= 1 || !isAsciiLetter(trimmed.charAt(0))) {
+            return null;
+        }
+        for (int i = 1; i < colon; i++) {
+            final char c = trimmed.charAt(i);
+            if (!isAsciiLetter(c) && !(c >= '0' && c <= '9') && c != '+' && c 
!= '-' && c != '.') {
+                return null;
+            }
+        }
+        return trimmed.substring(0, colon).toLowerCase(Locale.ENGLISH);
+    }
+
+    private static boolean isAsciiLetter(char c) {
+        return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
+    }
+
+    private static boolean isPlainRelativePath(String location) {
+        if (location.startsWith("/") || location.startsWith("\\")
+            || isWindowsDriveRootedPath(location)) {
+            return false;
+        }
+        for (String segment : location.replace('\\', '/').split("/")) {
+            if ("..".equals(segment)) {
+                return false;
+            }
+        }
+        return true;
+    }
 
+    private static boolean isWindowsDriveRootedPath(String location) {
+        return location.length() >= 3 && isAsciiLetter(location.charAt(0))
+            && location.charAt(1) == ':' && (location.charAt(2) == '/' || 
location.charAt(2) == '\\');
     }
 
     /**
diff --git a/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java 
b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
new file mode 100644
index 00000000..153a23ee
--- /dev/null
+++ b/xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests;
+
+import java.io.File;
+
+import org.apache.ws.commons.schema.XmlSchemaException;
+import org.apache.ws.commons.schema.resolver.DefaultURIResolver;
+import org.junit.Assert;
+import org.junit.Test;
+import org.xml.sax.InputSource;
+
+public class DefaultURIResolverTest extends Assert {
+
+    private static File existingDirectory() {
+        return new File(System.getProperty("java.io.tmpdir"));
+    }
+
+    @Test
+    public void testRemoteBaseIsNotRebasedOntoCollectionBase() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+        resolver.setCollectionBaseURI(existingDirectory().getAbsolutePath());
+
+        InputSource result = resolver.resolveEntity("urn:x", "sub/x.xsd",
+                                                    
"http://example.com/dir/remote.xsd";);
+
+        assertEquals("http://example.com/dir/sub/x.xsd";, result.getSystemId());
+    }
+
+    @Test
+    public void testAbsoluteFileLocationOnRemoteBaseIsRefused() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+
+        try {
+            resolver.resolveEntity("urn:x", "file:///etc/passwd",
+                                   "http://example.com/dir/remote.xsd";);
+            fail("An absolute file URL from a remote base must be refused.");
+        } catch (XmlSchemaException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testNonLocalFileAuthorityCannotTriggerCollectionRebase() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+        resolver.setCollectionBaseURI(existingDirectory().getAbsolutePath());
+
+        try {
+            InputSource result = resolver.resolveEntity("urn:x", 
"../secret.xsd",
+                                                        
"file://attacker.example/share/base.xsd");
+            assertFalse(result.getSystemId().startsWith("file:"));
+        } catch (XmlSchemaException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testJarFileAuthorityCannotTriggerCollectionRebase() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+        resolver.setCollectionBaseURI(existingDirectory().getAbsolutePath());
+
+        try {
+            InputSource result = resolver.resolveEntity("urn:x", 
"../secret.xsd",
+                                                        
"jar:file://attacker.example/share/a.jar!/dir/base.xsd");
+            assertFalse(result.getSystemId().startsWith("file:"));
+        } catch (XmlSchemaException expected) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testJarFileBaseStillResolvesInsideJar() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+        String jarBase = "jar:" + new File(existingDirectory(), 
"x.zip").toURI()
+            + "!/dir/base.xsd";
+
+        InputSource result = resolver.resolveEntity("urn:x", "child.xsd", 
jarBase);
+
+        assertTrue(result.getSystemId().startsWith("jar:file:"));
+        assertTrue(result.getSystemId().endsWith("/dir/child.xsd"));
+    }
+
+    @Test
+    public void testRootedWindowsLocationWithoutBaseIsRefused() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+
+        assertNull(resolver.resolveEntity("urn:x", 
"C:\\Windows\\System32\\config\\SAM", null));
+        assertNull(resolver.resolveEntity("urn:x", 
"C:/Windows/System32/config/SAM", null));
+    }
+
+    @Test
+    public void testPlainRelativeLocationWithoutBaseKeepsLegacyBehavior() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+
+        InputSource result = resolver.resolveEntity("urn:x", "sub/x.xsd", 
null);
+
+        assertEquals("sub/x.xsd", result.getSystemId());
+    }
+
+    @Test
+    public void testOpaqueBaseStillFallsBackToCollectionBase() {
+        DefaultURIResolver resolver = new DefaultURIResolver();
+        resolver.setCollectionBaseURI(existingDirectory().getAbsolutePath());
+
+        InputSource result = resolver.resolveEntity("urn:x", "imported.xsd", 
"urn:schemas0");
+
+        assertTrue(result.getSystemId().startsWith("file:"));
+        assertTrue(result.getSystemId().endsWith("imported.xsd"));
+    }
+}

Reply via email to