flawopen.com/Path Traversal/Kotlin

Path Traversal in Kotlin

High Severity CWE-22
ELI5

You ask for document 'invoice_102.pdf'. If someone passes 'invoice_102.pdf/../../../../etc/passwd', the Java File object walks back up into root system files unless you force it to resolve its canonical path first.

Key terms on this page
Canonical Path
The unique, absolute, symbolic-link-free representation of a file on the host operating system.
Path.normalize()
Eliminates redundant name elements (like '.' and '..') in Java NIO without touching the physical filesystem.

What's happening

In Java, new File(BASE_DIR, userInput) does not restrict files to BASE_DIR. An attacker submitting ../../../../etc/passwd causes the runtime to traverse outside the base path.

Real-world impact

Landmark vulnerabilities in enterprise Java software (including Jenkins, Apache Solr, and Jira plugins) have suffered arbitrary file read and write vulnerabilities via Java path traversal.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
// VULNERABLE: Direct File instantiation with user parameter
import java.io.File;
import javax.servlet.http.*;

public class FileServlet extends HttpServlet {
    private static final File BASE_DIR = new File("/var/app/public/files");

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String filename = req.getParameter("file");
        // Attacker sends: ../../../../etc/passwd
        File target = new File(BASE_DIR, filename);

        // Directly serves arbitrary system file!
        serveFile(target, resp);
    }
}
FIXED
// HARDENED: Canonicalize file and enforce directory prefix check
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import javax.servlet.http.*;

public class FileServlet extends HttpServlet {
    private static final File BASE_DIR = new File("/var/app/public/files");

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String filename = req.getParameter("file");
        if (filename == null || filename.isBlank()) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        // 1. Resolve canonical path (resolves .. and all symlinks)
        File target = new File(BASE_DIR, filename).getCanonicalFile();
        File canonicalBase = BASE_DIR.getCanonicalFile();

        // 2. Strict boundary check using Java NIO Path
        if (!target.toPath().startsWith(canonicalBase.toPath())) {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Path traversal detected");
            return;
        }

        if (!target.isFile()) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        serveFile(target, resp);
    }
}

Why the fix works

getCanonicalFile() resolves all symbolic links and dot-dot sequences into a definitive system path. target.toPath().startsWith(canonicalBase.toPath()) strictly ensures the resolved file resides inside the allowed directory hierarchy.

Gotchas

getAbsolutePath() vs getCanonicalPath()

getAbsolutePath() leaves '..' intact on non-existing paths; getCanonicalPath() resolves dots and symlinks via filesystem queries. Always use getCanonicalPath().

Common misconceptions

"FilenameUtils.getName() is always enough"

While Apache Commons FilenameUtils.getName() strips directory prefixes, it can prevent legitimate subfolder access within an allowed tree. Canonical checking is more flexible and robust.

How to check if you're affected

spotbugs -textui -include spotbugs-security.xml .

Prevention checklist

FAQ

How does Zip Slip affect Java?

Java's java.util.zip.ZipInputStream does not sanitize entry names. If a zip entry is named ../../evil.jsp, extract loops will write outside the target directory unless canonical checks are performed.

References