CVE-2021-32769 (published 2021-07-16T19:15Z)

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.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:P/I:N/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/micronaut-projects/micronaut-core
commit_details: repo_url: https://github.com/micronaut-projects/micronaut-core
author: Graeme Rocher
author_date: 2021-07-14 18:35:31+02:00
msg: 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>
repository: repo_url: https://github.com/micronaut-projects/micronaut-core
repo_name: micronaut-projects/micronaut-core
description: Micronaut Application Framework
repo_language: Java
owner: micronaut-projects
file_changes: filename: DefaultFileSystemResourceLoader.java
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,7 @@ import java.nio.file.Paths;
 import java.util.Optional;
 import java.util.stream.Stream;
 
+
 /**
  * Loads resources from the file system.
  *
@@ -36,54 +37,67 @@ import java.util.stream.Stream;
  */
 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();
@@ -103,6 +117,10 @@ public class DefaultFileSystemResourceLoader implements FileSystemResourceLoader
         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) {
@@ -115,6 +133,23 @@ public class DefaultFileSystemResourceLoader implements FileSystemResourceLoader
     }
 
     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;
     }
 }

programming_language: Java
filename: DefaultClassPathResourceLoader.java
change_type: ModificationType.MODIFY
diff: @@ -44,8 +44,10 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
 
     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.
@@ -65,6 +67,8 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
     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;
     }
 
     /**
@@ -75,53 +79,59 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
      */
     @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);
                 }
             }
         }
@@ -129,7 +139,23 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
         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());
+        }
     }
 
     /**
@@ -140,11 +166,17 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
      */
     @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();
     }
@@ -166,7 +198,9 @@ public class DefaultClassPathResourceLoader implements ClassPathResourceLoader {
         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();
     }

programming_language: Java
filename: ClasspathResourceLoaderSpec.groovy
change_type: ModificationType.MODIFY
diff: @@ -40,4 +40,23 @@ class ClasspathResourceLoaderSpec extends Specification {
         "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
+    }
 }

programming_language: Scala
filename: FileSystemResourceLoaderSpec.groovy
change_type: ModificationType.MODIFY
diff: @@ -18,25 +18,29 @@ 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
     }
 }

programming_language: Scala
filename: DefaultEnvironment.java
change_type: ModificationType.MODIFY
diff: @@ -453,7 +453,7 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement
                             }
                             order++;
                         } else {
-                            throw new ConfigurationException("Unsupported properties file format: " + fileName);
+                            throw new ConfigurationException("Unsupported properties file format: " + filePath);
                         }
                     }
                 }
@@ -583,12 +583,11 @@ public class DefaultEnvironment extends PropertySourcePropertyResolver implement
      * @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()) {

programming_language: Java
filename: DefaultEnvironmentSpec.groovy
change_type: ModificationType.MODIFY
diff: @@ -130,7 +130,7 @@ class DefaultEnvironmentSpec extends Specification {
 
         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")
@@ -197,7 +197,7 @@ class DefaultEnvironmentSpec extends Specification {
 
         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")

programming_language: Scala