CVE-2021-32769 (Other, published 2021-07-16T19:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/micronaut-projects/micronaut-core
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 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>

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