Zip Slip

Zip Slip (archive extraction path traversal)

Description

Zip Slip occurs when archive entry names such as entryName or fileName become output paths without validation. An attacker may use ../ traversal or absolute paths such as C:\ or /tmp to write outside the intended extraction directory. Depending on the destination and application permissions, this may plant executable files or overwrite configuration and disrupt the service.

Potential impact

  • File creation or overwriting with the application's permissions
  • Code execution if extracted files are later executed, such as scripts in a web root or startup directory
  • Outages from overwritten configuration, binaries, or logs
  • Corruption of application data or logs

Remediation

  • Normalize entry paths, reject absolute paths and parent-directory components, and check the final path.resolve result stays within the destination using path.relative.
  • Reject Windows drive prefixes such as C:\. Check ZIP external attributes and skip UNIX symbolic-link entries.
  • Extract only required extensions or directories through an allow-list.
  • Create a fresh private extraction directory under a trusted parent. Create directories with restricted permissions and use wx to prevent overwriting files.
  • Enable the archive library's entry filters and symbolic-link protections where available.

Examples

Before

javascript
// Before: extract using entry paths directly (Zip Slip).
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");

function extractUnsafe(zipPath, destDir) {
  const zip = new AdmZip(zipPath);
  zip.getEntries().forEach((entry) => {
    const outPath = path.join(destDir, entry.entryName); // No validation
    const data = entry.getData();
    // An entry.entryName of "../../../../var/www/html/shell.js" can escape the destination.
    fs.mkdirSync(path.dirname(outPath), { recursive: true });
    fs.writeFileSync(outPath, data); // Create or overwrite a file at an uncontrolled path.
  });
}

After

javascript
// After: validate entry types and final paths in a new private directory.
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");

function isUnixSymlink(entry) {
  // The high 16 bits of the ZIP external attributes contain the UNIX file mode.
  const unixMode = entry.header.attr >>> 16;
  return (unixMode & 0o170000) === 0o120000;
}

function extractSafe(zipPath, destParent) {
  const zip = new AdmZip(zipPath);
  const parentReal = fs.realpathSync(destParent);
  const destReal = fs.mkdtempSync(path.join(parentReal, "extract-"));
  fs.chmodSync(destReal, 0o700);

  for (const entry of zip.getEntries()) {
    // 1) Reject UNIX symbolic-link entries.
    if (isUnixSymlink(entry)) continue;

    // 2) Normalize the path.
    const normalized = path.normalize(entry.entryName);

    // 3) Reject empty/absolute paths, drive prefixes, and parent components.
    if (normalized === "" || normalized === ".") continue;
    if (path.isAbsolute(normalized)) continue;
    if (normalized.split(path.sep).includes("..")) continue;
    if (/^[A-Za-z]:/.test(normalized)) continue;

    // 4) Resolve the final path and check containment.
    const targetPath = path.resolve(destReal, normalized);
    if (
      path.relative(destReal, targetPath) !== ".." &&
      !path.relative(destReal, targetPath).startsWith(".." + path.sep) &&
      !path.isAbsolute(path.relative(destReal, targetPath))
    ) {
      if (entry.isDirectory) {
        fs.mkdirSync(targetPath, { recursive: true, mode: 0o700 });
        continue;
      }

      // 5) Create only within the new private directory, without overwriting files.
      fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
      const data = entry.getData();
      fs.writeFileSync(targetPath, data, { flag: "wx", mode: 0o600 });
    }
  }

  return destReal;
}

Explanation:

  • Before: Joining an unvalidated entryName with path.join allows ../ entries to escape the destination and create or overwrite files.
  • After: Use a new private directory under a trusted parent and reject UNIX symbolic-link entries. Normalize names, reject absolute paths, drive prefixes, and parent components, then check directory containment with path.resolve and path.relative. Write accepted files with wx so existing files are not overwritten.

References