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.resolveresult stays within the destination usingpath.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
wxto 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
entryNamewithpath.joinallows../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.resolveandpath.relative. Write accepted files withwxso existing files are not overwritten.