no-zip-slip
Detects zip slip/archive extraction vulnerabilities
CWE: CWE-693
OWASP Mobile: OWASP Mobile Top 10
Detects zip slip/archive extraction vulnerabilities. This rule is part of eslint-plugin-node-security.
š¼ This rule is set to error in the recommended config.
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-22 (Path Traversal) |
| Severity | High (CVSS 8.1) |
| Auto-Fix | š” Suggestions available |
| Category | Security |
Vulnerability and Risk
Vulnerability: Zip Slip is a form of directory traversal vulnerability that occurs during archive extraction. It allows an attacker to create files outside of the intended extraction directory by including directory traversal characters (e.g., ../) in the filenames within the archive.
Risk: An attacker can overwrite critical system files, configuration files, or application source code. This can lead to Remote Code Execution (RCE) if they overwrite an executable or a file that the application loads.
Rule Details
Zip slip vulnerabilities occur when extracting archives without properly validating file paths. Attackers can include files with path traversal sequences like ../ to write files outside the intended extraction directory, potentially:
- Overwrite critical system files
- Plant backdoors or web shells
- Replace application binaries
- Modify configuration files
Why This Matters
| Issue | Impact | Solution |
|---|---|---|
| š File Overwrite | System compromise | Validate extraction paths |
| šŖ Backdoor | Persistent access | Use safe extraction libraries |
| āļø Config Tampering | Application hijacking | Sandbox extraction directory |
Examples
ā Incorrect
// Extracting without path validation
const zip = new AdmZip(uploadedFile);
zip.extractAllTo(targetDir); // Vulnerable!
// Using entry path directly
for (const entry of archive.entries()) {
const outputPath = path.join(targetDir, entry.name);
fs.writeFileSync(outputPath, entry.getData()); // Vulnerable!
}
// tar extraction without validation
tar.extract({ file: uploadedTar, cwd: targetDir });ā Correct
const safeExtract = require('safe-archive-extract');
safeExtract(file, dest);
// Resolve, check containment, then use the checked path
for (const entry of archive.entries()) {
const outputPath = path.resolve(targetDir, entry.name);
if (!outputPath.startsWith(targetDir + path.sep)) {
throw new Error(`Zip Slip: ${entry.name}`);
}
fs.writeFileSync(outputPath, entry.getData());
}
// The path.relative() spelling of the same check
const rel = path.relative(targetDir, outputPath);
if (rel.startsWith('..') || path.isAbsolute(rel)) continue;A check validates only the branch where it passed. A join written in the branch
where it failed ā if (!p.startsWith(dest)) { write(path.join(dest, entry.name)) }
ā is still reported.
@safe
An @safe comment ā block (/** @safe */) or line (// @safe) ā covers the
statement directly below it, and everything inside that statement (so one on a
function covers the function). It does not cover the rest of the file: a
/** @safe */ in a file header vouches for the first statement only.
// @safe ā entries were verified by the upload service
zip.extractAllTo(dest);Configuration
{
rules: {
'node-security/no-zip-slip': ['error', {
archiveFunctions: ['extract', 'extractAll', 'extractAllTo', 'unzip'],
pathValidationFunctions: ['validatePath', 'isWithinDirectory'],
safeLibraries: ['safe-archive', 'secure-unzip']
}]
}
}Options
| Option | Type | Default | Description |
|---|---|---|---|
archiveFunctions | string[] | ["extract","extractAll","extractAllTo","unzip","untar","extractArchive"] | Archive extraction functions to check |
pathValidationFunctions | string[] | ["validatePath","sanitizePath","checkPath","safePath"] | Path validation functions |
safeLibraries | string[] | ["yauzl","safe-archive-extract","tar-stream","unzipper"] | Libraries with safe extraction |
archiveEntryFields | string[] | ["name","path","fileName","entryName","filename"] | Property names an archive entry exposes its path on. Replaces the default. |
archiveModules | string[] | ["adm-zip","unzipper","yauzl","yazl","tar","tar-fs","tar-stream","extract-zip","node-stream-zip","jszip","archiver","decompress","unzip-stream","zip-stream","gunzip-maybe","7zip-min","node-7z"] | Module specifiers that mean this file works with archives |
reportWithoutArchiveContext | boolean | false | Report entry-name and traversal shapes in files with no archive. Restores the pre-inversion behaviour. |
Error Message Format
The rule provides LLM-optimized error messages (Compact 2-line format) with actionable security guidance:
š CWE-22 OWASP:A01 CVSS:7.5 | Path Traversal detected | HIGH [SOC2,PCI-DSS,HIPAA,ISO27001]
Fix: Review and apply the recommended fix | https://owasp.org/Top10/A01_2021/Message Components
| Component | Purpose | Example |
|---|---|---|
| Risk Standards | Security benchmarks | CWE-22 OWASP:A01 CVSS:7.5 |
| Issue Description | Specific vulnerability | Path Traversal detected |
| Severity & Compliance | Impact assessment | HIGH [SOC2,PCI-DSS,HIPAA,ISO27001] |
| Fix Instruction | Actionable remediation | Follow the remediation steps below |
| Technical Truth | Official reference | OWASP Top 10 |
Known False Negatives
The following patterns are not detected due to static analysis limitations:
Values from Variables
Why: Values stored in variables are not traced.
// ā NOT DETECTED - Value from variable
const value = userInput;
dangerousOperation(value);Mitigation: Validate all user inputs.
Wrapper Functions
Why: Custom wrappers not recognized.
// ā NOT DETECTED - Wrapper
myWrapper(userInput); // Uses dangerous API internallyMitigation: Apply rule to wrapper implementations.
Dynamic Invocation
Why: Dynamic calls not analyzed.
// ā NOT DETECTED - Dynamic
obj[method](userInput);Mitigation: Avoid dynamic method invocation.
Further Reading
- Snyk Zip Slip Research - Original research
- CWE-22 - Path traversal documentation
- OWASP Path Traversal - Attack techniques
Related Rules
detect-non-literal-fs-filename- Path traversal in fs operationsno-toctou-vulnerability- Race condition vulnerabilities
Use this rule
npm install -D oxlint-plugin-node-security{ "jsPlugins": ["oxlint-plugin-node-security"], "rules": { "node-security/no-zip-slip": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-node-security'], rules: { 'node-security/no-zip-slip': 'error', }, },});oxlint-plugin-node-security re-exports eslint-plugin-node-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it ā or follow the AI-code-security benchmarks behind these rules.