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