Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@
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;

/**
Expand All @@ -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) == '\\');
}

/**
Expand Down
126 changes: 126 additions & 0 deletions xmlschema-core/src/test/java/tests/DefaultURIResolverTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading