CVE-2021-32769: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Micronaut is a JVM-based, full stack Java framework designed for building JVM applications. A path traversal vulnerability exists in versions prior to 2.5.9. With a basic configuration, it is possible to access any file from a filesystem, using "/../../" in the URL. This occurs because Micronaut does not restrict file access to configured paths. The vulnerability is patched in version 2.5.9. As a workaround, do not use `**` in mapping, use only `*`, which exposes only flat structure of a directory not allowing traversal. If using Linux, another workaround is to run micronaut in chroot.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.5
Published: 2021-07-16T19:15Z
Repository: https://github.com/micronaut-projects/micronaut-core
Language: Other
Files changed: core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java, core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java, core/src/test/groovy/io/micronaut/core/io/ClasspathResourceLoaderSpec.groovy, core/src/test/groovy/io/micronaut/core/io/FileSystemResourceLoaderSpec.groovy, inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java, inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy
Commit date: 2021-07-14 12:35:31 -0400
Commit message: Merge pull request from GHSA-cjx7-399x-p2rj

* Don't allow resolving files using relative paths outside of the base

* Revert change around reading properties files

Co-authored-by: Lukas, Strmiska <strmik@gmail.com>
Co-authored-by: jameskleeh <james.kleeh@gmail.com>
Diff: diff --git a/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java b/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java
index 2f5a914039..4670189574 100644
--- a/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java
+++ b/core/src/main/java/io/micronaut/core/io/file/DefaultFileSystemResourceLoader.java
@@ -26,66 +26,80 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Optional;
 import java.util.stream.Stream;
 
+
 /**
  * Loads resources from the file system.
  *
  * @author James Kleeh
  * @since 1.0
  */
 public class DefaultFileSystemResourceLoader implements FileSystemResourceLoader {
 
-    private final Optional<Path> baseDirPath;
+    private final Path baseDirPath;
+    private final boolean baseExists;
 
     /**
      * Default constructor.
      */
     public DefaultFileSystemResourceLoader() {
-        this.baseDirPath = Optional.empty();
+        this.baseDirPath = null;
+        this.baseExists = true;
     }
 
     /**
      * @param baseDirPath The base directory
      */
     public DefaultFileSystemResourceLoader(File baseDirPath) {
-        this.baseDirPath = Optional.of(baseDirPath.toPath());
+        this(baseDirPath.toPath().normalize());
     }
 
     /**
      * @param path The path
      */
     public DefaultFileSystemResourceLoader(String path) {
-        this.baseDirPath = Optional.of(Paths.get(normalize(path)));
+        this(Paths.get(normalize(path)));
     }
 
     /**
      * @param path The path
      */
     public DefaultFileSystemResourceLoader(Path path) {
-        this.baseDirPath = Optional.of(path);
+        Path baseDirPath;
+        try {
+            baseDirPath = path.normalize().toRealPath();
+        } catch (IOException e) {
+            baseDirPath = null;
+        }
+        this.baseExists = baseDirPath != null;
+        this.baseDirPath = baseDirPath;
     }
 
     @Override
     public Optional<InputStream> getResourceAsStream(String path) {
         Path filePath = getFilePath(normalize(path));
-        try {
-            return Optional.of(Files.newInputStream(filePath));
-        } catch (IOException e) {
-            return Optional.empty();
+        if (isResolvableFile(filePath)) {
+            try {
+                return Optional.of(Files.newInputStream(filePath));
+            } catch (IOException e) {
+                return Optional.empty();
+            }
         }
+        return Optional.empty();
     }
 
     @Override
     public Optional<URL> getResource(String path) {
         Path filePath = getFilePath(normalize(path));
-        if (Files.exists(filePath) && Files.isReadable(filePath) && !Files.isDirectory(filePath)) {
+        if (isResolvableFile(filePath)) {
             try {
                 URL url = filePath.toUri().toURL();
                 return Optional.of(url);
             } catch (MalformedURLException e) {
+                // ignore
             }
         }
         return Optional.empty();
     }
 
@@ -101,10 +115,14 @@ public class DefaultFileSystemResourceLoader implements FileSystemResourceLoader
     @Override
     public ResourceLoader forBase(String basePath) {
         return new DefaultFileSystemResourceLoader(basePath);
     }
 
+    private boolean isResolvableFile(Path filePath) {
+        return startsWithBase(filePath) && Files.exists(filePath) && Files.isReadable(filePath) && !Files.isDirectory(filePath);
+    }
+
     @SuppressWarnings("MagicNumber")
     private static String normalize(String path) {
         if (path == null) {
             return null;
         }
@@ -113,8 +131,25 @@ public class DefaultFileSystemResourceLoader implements FileSystemResourceLoader
         }
         return path;
     }
 
     private Path getFilePath(String path) {
-        return baseDirPath.map(dir -> dir.resolve(path)).orElseGet(() -> Paths.get(path));
+        if (baseDirPath != null) {
+            return baseDirPath.resolve(path);
+        } else {
+            return Paths.get(path);
+        }
+    }
+
+    private boolean startsWithBase(Path path) {
+        if (baseDirPath != null) {
+            Path relativePath;
+            try {
+                relativePath = baseDirPath.resolve(path).toRealPath();
+                return relativePath.startsWith(baseDirPath);
+            } catch (IOException e) {
+                return false;
+            }
+        }
+        return baseExists;
     }
 }
diff --git a/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java b/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java
index d7739579f3..ebb04115c3 100644
--- a/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java
+++ b/core/src/main/java/io/micronaut/core/io/scan/DefaultClassPathResourceLoader.java
@@ -42,12 +42,14 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
 
     private static final Logger LOG = LoggerFactory.getLogger(DefaultClassPathResourceLoader.class);
 
     private final ClassLoader classLoader;
     private final String basePath;
+    private final URL baseURL;
     private final Map<String, Boolean> isDirectoryCache = new ConcurrentLinkedHashMap.Builder<String, Boolean>()
             .maximumWeightedCapacity(50).build();
+    private final boolean missingPath;
 
     /**
      * Default constructor.
      *
      * @param classLoader The class loader for loading resources
@@ -63,90 +65,120 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
      * @param basePath    The path to look for resources under
      */
     public DefaultClassPathResourceLoader(ClassLoader classLoader, String basePath) {
         this.classLoader = classLoader;
         this.basePath = normalize(basePath);
+        this.baseURL = basePath != null ? classLoader.getResource(normalize(basePath)) : null;
+        this.missingPath = basePath != null && baseURL == null;
     }
 
     /**
      * Obtains a resource as a stream.
      *
      * @param path The path
      * @return An optional resource
      */
     @Override
     public Optional<InputStream> getResourceAsStream(String path) {
+        if (missingPath) {
+            return Optional.empty();
+        }
+
         URL url = classLoader.getResource(prefixPath(path));
         if (url != null) {
-            try {
-                URI uri = url.toURI();
-                if (uri.getScheme().equals("jar")) {
-                    synchronized (DefaultClassPathResourceLoader.class) {
-                        FileSystem fileSystem = null;
-                        try {
+            if (startsWithBase(url)) {
+                try {
+                    URI uri = url.toURI();
+                    if (uri.getScheme().equals("jar")) {
+                        synchronized (DefaultClassPathResourceLoader.class) {
+                            FileSystem fileSystem = null;
                             try {
-                                fileSystem = FileSystems.getFileSystem(uri);
-                            } catch (FileSystemNotFoundException e) {
-                                //no-op
-                            }
-                            if (fileSystem == null || !fileSystem.isOpen()) {
                                 try {
-                                    fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap(), classLoader);
-                                } catch (FileSystemAlreadyExistsException e) {
                                     fileSystem = FileSystems.getFileSystem(uri);
+                                } catch (FileSystemNotFoundException e) {
+                                    //no-op
                                 }
-                            }
-                            Path pathObject = fileSystem.getPath(path);
-                            if (Files.isDirectory(pathObject)) {
-                                return Optional.empty();
-                            }
-                            return Optional.of(new ByteArrayInputStream(Files.readAllBytes(pathObject)));
-                        } finally {
-                            if (fileSystem != null && fileSystem.isOpen()) {
-                                try {
-                                    fileSystem.close();
-                                } catch (IOException e) {
-                                    if (LOG.isDebugEnabled()) {
-                                        LOG.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e);
+                                if (fileSystem == null || !fileSystem.isOpen()) {
+                                    try {
+                                        fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap(), classLoader);
+                                    } catch (FileSystemAlreadyExistsException e) {
+                                        fileSystem = FileSystems.getFileSystem(uri);
+                                    }
+                                }
+                                Path pathObject = fileSystem.getPath(path);
+                                if (Files.isDirectory(pathObject)) {
+                                    return Optional.empty();
+                                }
+                                return Optional.of(new ByteArrayInputStream(Files.readAllBytes(pathObject)));
+                            } finally {
+                                if (fileSystem != null && fileSystem.isOpen()) {
+                                    try {
+                                        fileSystem.close();
+                                    } catch (IOException e) {
+                                        if (LOG.isDebugEnabled()) {
+                                            LOG.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e);
+                                        }
                                     }
                                 }
                             }
                         }
+                    } else if (uri.getScheme().equals("file")) {
+                        Path pathObject = Paths.get(uri);
+                        if (Files.isDirectory(pathObject)) {
+                            return Optional.empty();
+                        }
+                        return Optional.of(Files.newInputStream(pathObject));
                     }
-                } else if (uri.getScheme().equals("file")) {
-                    Path pathObject = Paths.get(uri);
-                    if (Files.isDirectory(pathObject)) {
-                        return Optional.empty();
+                } catch (URISyntaxException | IOException | ProviderNotFoundException e) {
+                    if (LOG.isDebugEnabled()) {
+                        LOG.debug("Error establishing whether path is a directory: " + e.getMessage(), e);
                     }
-                    return Optional.of(Files.newInputStream(pathObject));
-                }
-            } catch (URISyntaxException | IOException | ProviderNotFoundException e) {
-                if (LOG.isDebugEnabled()) {
-                    LOG.debug("Error establishing whether path is a directory: " + e.getMessage(), e);
                 }
             }
         }
         // fallback to less sophisticated approach
         if (path.indexOf('.') == -1) {
             return Optional.empty();
         }
-        return Optional.ofNullable(classLoader.getResourceAsStream(prefixPath(path)));
+        final URL u = getResource(path).orElse(null);
+        if (u != null) {
+            try {
+                return Optional.of(u.openStream());
+            } catch (IOException e) {
+                // fallback to empty
+            }
+        }
+        return Optional.empty();
+    }
+
+    private boolean startsWithBase(URL url) {
+        if (baseURL == null) {
+            return true;
+        } else {
+            return url.toExternalForm().startsWith(baseURL.toExternalForm());
+        }
     }
 
     /**
      * Obtains a resource URL.
      *
      * @param path The path
      * @return An optional resource
      */
     @Override
     public Optional<URL> getResource(String path) {
+        if (missingPath) {
+            return Optional.empty();
+        }
+
         boolean isDirectory = isDirectory(path);
 
         if (!isDirectory) {
             URL url = classLoader.getResource(prefixPath(path));
-            return Optional.ofNullable(url);
+            if (url != null && startsWithBase(url)) {
+                return Optional.of(url);
+            }
         }
         return Optional.empty();
     }
 
     /**
@@ -164,11 +196,13 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
             return Stream.empty();
         }
         Stream.Builder<URL> builder = Stream.builder();
         while (all.hasMoreElements()) {
             URL url = all.nextElement();
-            builder.accept(url);
+            if (startsWithBase(url)) {
+                builder.accept(url);
+            }
         }
         return builder.build();
     }
 
     /**
diff --git a/core/src/test/groovy/io/micronaut/core/io/ClasspathResourceLoaderSpec.groovy b/core/src/test/groovy/io/micronaut/core/io/ClasspathResourceLoaderSpec.groovy
index b873e0a4f6..f45e23530f 100644
--- a/core/src/test/groovy/io/micronaut/core/io/ClasspathResourceLoaderSpec.groovy
+++ b/core/src/test/groovy/io/micronaut/core/io/ClasspathResourceLoaderSpec.groovy
@@ -38,6 +38,25 @@ class ClasspathResourceLoaderSpec extends Specification {
         "classpath:foo"  | "classpath:bar.txt"
         "classpath:/foo" | "classpath:bar.txt"
         "classpath:/foo" | "classpath:/bar.txt"
         "classpath:foo"  | "classpath:/bar.txt"
     }
+
+    void "test resolving a classpath resource with a relative path"() {
+        given:
+        ClassPathResourceLoader loader = new DefaultClassPathResourceLoader(getClass().getClassLoader(), base)
+
+        expect:
+        loader.getResource(resource).isPresent() == present
+        loader.getResourceAsStream(resource).map({ InputStream io -> io.close(); io})
+                .isPresent() == present
+        loader.getResources(resource).findFirst().isPresent() == present
+
+        where:
+        base             | resource                                        | present
+        "classpath:foo"  | "classpath:../foo/bar.txt"                      | true
+        "classpath:foo"  | "classpath:../other/../foo/bar.txt"             | true
+        "classpath:foo"  | "classpath:../foo/../other/shouldNotAccess.txt" | false
+        "classpath:foo"  | "classpath:../other/shouldNotAccess.txt"        | false
+        "classpath:foo"  | "classpath:foo/../../other/shouldNotAccess.txt" | false
+    }
 }
diff --git a/core/src/test/groovy/io/micronaut/core/io/FileSystemResourceLoaderSpec.groovy b/core/src/test/groovy/io/micronaut/core/io/FileSystemResourceLoaderSpec.groovy
index c82b76036a..4b3761f1a9 100644
--- a/core/src/test/groovy/io/micronaut/core/io/FileSystemResourceLoaderSpec.groovy
+++ b/core/src/test/groovy/io/micronaut/core/io/FileSystemResourceLoaderSpec.groovy
@@ -16,27 +16,31 @@
 package io.micronaut.core.io
 
 import io.micronaut.core.io.file.DefaultFileSystemResourceLoader
 import io.micronaut.core.io.file.FileSystemResourceLoader
 import spock.lang.Specification
-
-import java.nio.file.Paths
+import spock.lang.Unroll
 
 class FileSystemResourceLoaderSpec extends Specification {
 
-    void "test resolving a resource"() {
+    @Unroll
+    void "test resolving a resource for #base and #resource"() {
         given:
-        FileSystemResourceLoader loader = new DefaultFileSystemResourceLoader(base)
+        DefaultFileSystemResourceLoader loader = new DefaultFileSystemResourceLoader(base)
 
         expect:
-        !loader.getResource(resource).isPresent()
+        loader.getResource(resource).isPresent() == presentViaResolver
+        new File(loader.normalize(base), loader.normalize(resource)).exists() == presentOnDisk
 
         where:
-        base        | resource
-        "."         | "src"
-        "."         | "file:src/main"
-        "src"       | "main"
-        "file:src"  | "main"
-        "file:src"  | "file:main"
-        "file:src"  | "file:/main"
+        base                      | resource                   | presentViaResolver   | presentOnDisk
+        "."                       | "src"                      | false                | true
+        "."                       | "file:src/main"            | false                | true
+        "src"                     | "main"                     | false                | true
+        "file:src"                | "main"                     | false                | true
+        "file:src"                | "file:main"                | false                | true
+        "file:src"                | "file:/main"               | false                | true
+        "src/test/resources"      | "foo/bar.txt"              | true                 | true
+        "file:src/test/resources" | "../resources/foo/bar.txt" | true                 | true
+        "file:src/test/resources" | "../../../build.gradle"    | false                | true
     }
 }
diff --git a/core/src/test/resources/other/shouldNotAccess.txt b/core/src/test/resources/other/shouldNotAccess.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java
index 9aa7ba2bcc..40539f54a7 100644
--- a/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java
+++ b/inject/src/main/java/io/micronaut/context/env/DefaultEnvironment.java
@@ -451,11 +451,11 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement
                             if (properties.isPresent()) {
                                 propertySources.add(PropertySource.of(filePath, properties.get(), order));
                             }
                             order++;
                         } else {
-                            throw new ConfigurationException("Unsupported properties file format: " + fileName);
+                            throw new ConfigurationException("Unsupported properties file format: " + filePath);
                         }
                     }
                 }
             }
         });
@@ -581,16 +581,15 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement
      * Read the property source.
      *
      * @param fileName             Name of the file to be used as property source name
      * @param filePath             Absolute file path
      * @param propertySourceLoader The appropriate property source loader
-     * @throws ConfigurationException If unable to find the appropriate property soruce loader for the given file
+     * @throws ConfigurationException If unable to find the appropriate property source loader for the given file
      */
     private Optional<Map<String, Object>> readPropertiesFromLoader(String fileName, String filePath, PropertySourceLoader propertySourceLoader) throws ConfigurationException {
-        ResourceResolver resourceResolver = new ResourceResolver();
-        Optional<ResourceLoader> resourceLoader = resourceResolver.getSupportingLoader(filePath);
-        ResourceLoader loader = resourceLoader.orElse(FileSystemResourceLoader.defaultLoader());
+        ResourceLoader loader = new ResourceResolver().getSupportingLoader(filePath)
+                .orElse(FileSystemResourceLoader.defaultLoader());
         try {
             Optional<InputStream> inputStream = loader.getResourceAsStream(filePath);
             if (inputStream.isPresent()) {
                 return Optional.of(propertySourceLoader.read(fileName, inputStream.get()));
             } else {
diff --git a/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy b/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy
index 1a471cb662..8285af63b1 100644
--- a/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy
+++ b/inject/src/test/groovy/io/micronaut/context/env/DefaultEnvironmentSpec.groovy
@@ -128,11 +128,11 @@ class DefaultEnvironmentSpec extends Specification {
         and: "load the property sources"
         env.refresh()
 
         then: "should throw exception"
         def e = thrown(ConfigurationException)
-        e.message == "Unsupported properties file format: " + NameUtils.filename(unsupportedFile.absolutePath)
+        e.message == "Unsupported properties file format: " + unsupportedFile.absolutePath
 
         when: "file from system property source loader does not override the key"
         System.setProperty("foo.baz", "10")
         configPropertiesFile = File.createTempFile("config-file", ".properties")
         configPropertiesFile.write("foo.baz=50")
@@ -195,11 +195,11 @@ class DefaultEnvironmentSpec extends Specification {
         and: "load the property sources"
         startEnv("${unsupportedFile.absolutePath}")
 
         then: "should throw exception"
         def e = thrown(ConfigurationException)
-        e.message == "Unsupported properties file format: " + NameUtils.filename(unsupportedFile.absolutePath)
+        e.message == "Unsupported properties file format: " + unsupportedFile.absolutePath
 
         when: "file from system property source loader does not override the key"
         System.setProperty("foo.baz", "10")
         configPropertiesFile = File.createTempFile("config-file", ".properties")
         configPropertiesFile.write("foo.baz=50")

Vulnerable code:     private final Optional<Path> baseDirPath;
        this.baseDirPath = Optional.empty();
        this.baseDirPath = Optional.of(baseDirPath.toPath());
        this.baseDirPath = Optional.of(Paths.get(normalize(path)));
        this.baseDirPath = Optional.of(path);
        try {
            return Optional.of(Files.newInputStream(filePath));
        } catch (IOException e) {
            return Optional.empty();
        if (Files.exists(filePath) && Files.isReadable(filePath) && !Files.isDirectory(filePath)) {
        return baseDirPath.map(dir -> dir.resolve(path)).orElseGet(() -> Paths.get(path));
            try {
                URI uri = url.toURI();
                if (uri.getScheme().equals("jar")) {
                    synchronized (DefaultClassPathResourceLoader.class) {
                        FileSystem fileSystem = null;
                        try {
                                fileSystem = FileSystems.getFileSystem(uri);
                            } catch (FileSystemNotFoundException e) {
                                //no-op
                            }
                            if (fileSystem == null || !fileSystem.isOpen()) {
                                    fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap(), classLoader);
                                } catch (FileSystemAlreadyExistsException e) {
                            }
                            Path pathObject = fileSystem.getPath(path);
                            if (Files.isDirectory(pathObject)) {
                                return Optional.empty();
                            }
                            return Optional.of(new ByteArrayInputStream(Files.readAllBytes(pathObject)));
                        } finally {
                            if (fileSystem != null && fileSystem.isOpen()) {
                                try {
                                    fileSystem.close();
                                } catch (IOException e) {
                                    if (LOG.isDebugEnabled()) {
                                        LOG.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e);
                } else if (uri.getScheme().equals("file")) {
                    Path pathObject = Paths.get(uri);
                    if (Files.isDirectory(pathObject)) {
                        return Optional.empty();
                    return Optional.of(Files.newInputStream(pathObject));
                }
            } catch (URISyntaxException | IOException | ProviderNotFoundException e) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Error establishing whether path is a directory: " + e.getMessage(), e);
        return Optional.ofNullable(classLoader.getResourceAsStream(prefixPath(path)));
            return Optional.ofNullable(url);
            builder.accept(url);

import java.nio.file.Paths
    void "test resolving a resource"() {
        FileSystemResourceLoader loader = new DefaultFileSystemResourceLoader(base)
        !loader.getResource(resource).isPresent()
        base        | resource
        "."         | "src"
        "."         | "file:src/main"
        "src"       | "main"
        "file:src"  | "main"
        "file:src"  | "file:main"
        "file:src"  | "file:/main"
                            throw new ConfigurationException("Unsupported properties file format: " + fileName);
     * @throws ConfigurationException If unable to find the appropriate property soruce loader for the given file
        ResourceResolver resourceResolver = new ResourceResolver();
        Optional<ResourceLoader> resourceLoader = resourceResolver.getSupportingLoader(filePath);
        ResourceLoader loader = resourceLoader.orElse(FileSystemResourceLoader.defaultLoader());
        e.message == "Unsupported properties file format: " + NameUtils.filename(unsupportedFile.absolutePath)
        e.message == "Unsupported properties file format: " + NameUtils.filename(unsupportedFile.absolutePath)
Fixed code: 
    private final Path baseDirPath;
    private final boolean baseExists;
        this.baseDirPath = null;
        this.baseExists = true;
        this(baseDirPath.toPath().normalize());
        this(Paths.get(normalize(path)));
        Path baseDirPath;
        try {
            baseDirPath = path.normalize().toRealPath();
        } catch (IOException e) {
            baseDirPath = null;
        }
        this.baseExists = baseDirPath != null;
        this.baseDirPath = baseDirPath;
        if (isResolvableFile(filePath)) {
            try {
                return Optional.of(Files.newInputStream(filePath));
            } catch (IOException e) {
                return Optional.empty();
            }
        return Optional.empty();
        if (isResolvableFile(filePath)) {
                // ignore
    private boolean isResolvableFile(Path filePath) {
        return startsWithBase(filePath) && Files.exists(filePath) && Files.isReadable(filePath) && !Files.isDirectory(filePath);
    }

        if (baseDirPath != null) {
            return baseDirPath.resolve(path);
        } else {
            return Paths.get(path);
        }
    }

    private boolean startsWithBase(Path path) {
        if (baseDirPath != null) {
            Path relativePath;
            try {
                relativePath = baseDirPath.resolve(path).toRealPath();
                return relativePath.startsWith(baseDirPath);
            } catch (IOException e) {
                return false;
            }
        }
        return baseExists;
    private final URL baseURL;
    private final boolean missingPath;
        this.baseURL = basePath != null ? classLoader.getResource(normalize(basePath)) : null;
        this.missingPath = basePath != null && baseURL == null;
        if (missingPath) {
            return Optional.empty();
        }

            if (startsWithBase(url)) {
                try {
                    URI uri = url.toURI();
                    if (uri.getScheme().equals("jar")) {
                        synchronized (DefaultClassPathResourceLoader.class) {
                            FileSystem fileSystem = null;
                                } catch (FileSystemNotFoundException e) {
                                    //no-op
                                if (fileSystem == null || !fileSystem.isOpen()) {
                                    try {
                                        fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap(), classLoader);
                                    } catch (FileSystemAlreadyExistsException e) {
                                        fileSystem = FileSystems.getFileSystem(uri);
                                    }
                                }
                                Path pathObject = fileSystem.getPath(path);
                                if (Files.isDirectory(pathObject)) {
                                    return Optional.empty();
                                }
                                return Optional.of(new ByteArrayInputStream(Files.readAllBytes(pathObject)));
                            } finally {
                                if (fileSystem != null && fileSystem.isOpen()) {
                                    try {
                                        fileSystem.close();
                                    } catch (IOException e) {
                                        if (LOG.isDebugEnabled()) {
                                            LOG.debug("Error shutting down JAR file system [" + fileSystem + "]: " + e.getMessage(), e);
                                        }
                    } else if (uri.getScheme().equals("file")) {
                        Path pathObject = Paths.get(uri);
                        if (Files.isDirectory(pathObject)) {
                            return Optional.empty();
                        }
                        return Optional.of(Files.newInputStream(pathObject));
                } catch (URISyntaxException | IOException | ProviderNotFoundException e) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Error establishing whether path is a directory: " + e.getMessage(), e);
        final URL u = getResource(path).orElse(null);
        if (u != null) {
            try {
                return Optional.of(u.openStream());
            } catch (IOException e) {
                // fallback to empty
            }
        }
        return Optional.empty();
    }

    private boolean startsWithBase(URL url) {
        if (baseURL == null) {
            return true;
        } else {
            return url.toExternalForm().startsWith(baseURL.toExternalForm());
        }
        if (missingPath) {
            return Optional.empty();
        }

            if (url != null && startsWithBase(url)) {
                return Optional.of(url);
            }
            if (startsWithBase(url)) {
                builder.accept(url);
            }

    void "test resolving a classpath resource with a relative path"() {
        given:
        ClassPathResourceLoader loader = new DefaultClassPathResourceLoader(getClass().getClassLoader(), base)

        expect:
        loader.getResource(resource).isPresent() == present
        loader.getResourceAsStream(resource).map({ InputStream io -> io.close(); io})
                .isPresent() == present
        loader.getResources(resource).findFirst().isPresent() == present

        where:
        base             | resource                                        | present
        "classpath:foo"  | "classpath:../foo/bar.txt"                      | true
        "classpath:foo"  | "classpath:../other/../foo/bar.txt"             | true
        "classpath:foo"  | "classpath:../foo/../other/shouldNotAccess.txt" | false
        "classpath:foo"  | "classpath:../other/shouldNotAccess.txt"        | false
        "classpath:foo"  | "classpath:foo/../../other/shouldNotAccess.txt" | false
    }
import spock.lang.Unroll
    @Unroll
    void "test resolving a resource for #base and #resource"() {
        DefaultFileSystemResourceLoader loader = new DefaultFileSystemResourceLoader(base)
        loader.getResource(resource).isPresent() == presentViaResolver
        new File(loader.normalize(base), loader.normalize(resource)).exists() == presentOnDisk
        base                      | resource                   | presentViaResolver   | presentOnDisk
        "."                       | "src"                      | false                | true
        "."                       | "file:src/main"            | false                | true
        "src"                     | "main"                     | false                | true
        "file:src"                | "main"                     | false                | true
        "file:src"                | "file:main"                | false                | true
        "file:src"                | "file:/main"               | false                | true
        "src/test/resources"      | "foo/bar.txt"              | true                 | true
        "file:src/test/resources" | "../resources/foo/bar.txt" | true                 | true
        "file:src/test/resources" | "../../../build.gradle"    | false                | true
                            throw new ConfigurationException("Unsupported properties file format: " + filePath);
     * @throws ConfigurationException If unable to find the appropriate property source loader for the given file
        ResourceLoader loader = new ResourceResolver().getSupportingLoader(filePath)
                .orElse(FileSystemResourceLoader.defaultLoader());
        e.message == "Unsupported properties file format: " + unsupportedFile.absolutePath
        e.message == "Unsupported properties file format: " + unsupportedFile.absolutePath