Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. Zip Slip (Insecure Archive Extraction)

Zip Slip (Insecure Archive Extraction)

Here is an example of Java code that is vulnerable to the Zip Slip archive extraction flaw:

🥺 Vulnerable Code

try (ZipInputStream zis = new ZipInputStream(uploaded.getInputStream())) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {

        // Vulnerable: the entry name inside the archive is trusted
        File outFile = new File(UPLOAD_DIR, entry.getName());

        try (FileOutputStream fos = new FileOutputStream(outFile)) {
            zis.transferTo(fos);
        }
    }
}

Archive entry names are attacker controlled strings, and they are allowed to contain ../. An entry called ../../../../opt/app/webapps/ROOT/shell.jsp escapes the upload directory and lands in the web root, which is remote code execution from a file upload form. The same loop happily overwrites configuration files, SSH keys, and cron entries, and it has no limit on how much data a small archive expands into.

😎 Secure Code

Here is a version of the same code that is secured against the Zip Slip archive extraction flaw:

Path targetDir = Paths.get(UPLOAD_DIR).toAbsolutePath().normalize();
long totalBytes = 0;

try (ZipInputStream zis = new ZipInputStream(uploaded.getInputStream())) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        if (entry.isDirectory()) {
            continue;
        }

        Path resolved = targetDir.resolve(entry.getName()).normalize();
        if (!resolved.startsWith(targetDir)) {
            throw new SecurityException("Blocked traversal in archive: " + entry.getName());
        }

        totalBytes += Math.max(entry.getSize(), 0);
        if (totalBytes > MAX_TOTAL_BYTES) {
            throw new SecurityException("Archive expands beyond the allowed size");
        }

        Files.createDirectories(resolved.getParent());
        try (OutputStream out = Files.newOutputStream(resolved, StandardOpenOption.CREATE_NEW)) {
            zis.transferTo(out);
        }
    }
}

Resolving and normalizing the path and then checking that it still starts with the target directory is the control that actually stops traversal - string checks for .. are easy to bypass with encoding and backslashes. CREATE_NEW refuses to overwrite existing files, the running total stops zip bombs, and extracting into a non-executable directory outside the web root means even a planted script cannot be requested by a browser.