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 12ca3b70 Restrict default protocols allowed by the DefaultURIResolver
(#148)
12ca3b70 is described below
commit 12ca3b70dc745c26b6f3a825924e9d3b80148a47
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed Sep 16 10:27:03 2026 +0100
Restrict default protocols allowed by the DefaultURIResolver (#148)
---
.../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());
+ }
}