Zip Slip in Feathersjs
How Zip Slip Manifests in Feathersjs
Zip Slip is a directory traversal vulnerability that occurs when an application extracts ZIP archives without validating file paths. In Feathersjs applications, this typically manifests through file upload endpoints that process user-supplied archives.
The vulnerability allows attackers to craft ZIP files with malicious paths like ../../etc/passwd or ../../../../tmp/evil.sh. When Feathersjs extracts these archives using Node.js's built-in zlib or adm-zip libraries without path validation, files are written outside the intended directory.
Common Feathersjs attack vectors include:
- File upload services using
multeror custom middleware that accept ZIP files - Document management services that process user-submitted archives
- Plugin systems that load ZIP-based extensions
- Backup/restore endpoints that unpack archives
- Template engines that process ZIP-based themes
The impact is severe: attackers can overwrite critical system files, plant web shells in web-accessible directories, or execute arbitrary code by placing scripts in executable locations.
Real-world Feathersjs code vulnerable to Zip Slip might look like:
const fs = require('fs');
const AdmZip = require('adm-zip');
class FileUploadService {
async uploadZip(file) {
const zip = new AdmZip(file.buffer);
zip.extractAllTo("./uploads", true); // UNSAFE: no path validation
}
}
module.exports = FileUploadService;This code directly extracts to the uploads directory without validating that extracted paths stay within that directory. An attacker could create a ZIP with ../../config/database.json to overwrite configuration files.
Another Feathersjs-specific pattern involves hooks that process uploaded files:
const { hooks } = require('feathers-authentication');
module.exports = {
before: {
create: [hooks.authenticate('jwt'), validateZip]
},
async upload(context) {
const { file } = context.data;
const zip = AdmZip(file.buffer);
zip.extractAllTo("./user-content", true); // VULNERABLE
return context;
}
};The authentication hook provides a false sense of security—Zip Slip doesn't require authentication to exploit, making this particularly dangerous.
Feathersjs-Specific Detection
Detecting Zip Slip in Feathersjs applications requires examining both code patterns and runtime behavior. Static analysis should focus on these Feathersjs-specific indicators:
- Services with
upload,archive,extract, orzipin their names - Hook chains that process file data before validation
- Configuration files showing
multerwithstorage: diskStorageand no path sanitization - Package.json dependencies including
adm-zip,yauzl, ornode-zipwithout path validation
Runtime detection with middleBrick specifically identifies Feathersjs Zip Slip vulnerabilities by:
- Scanning API endpoints that accept multipart/form-data with file parameters
- Testing for directory traversal in file extraction endpoints
- Analyzing OpenAPI specs for endpoints with
application/zipcontent types - Checking for unsafe use of Node.js file system operations in service methods
Here's how to scan a Feathersjs API with middleBrick:
# Install the CLI
npm install -g middlebrick
# Scan your Feathersjs API
middlebrick scan https://api.yourservice.com
# Or integrate into your Feathersjs project
middlebrick scan http://localhost:3030
middleBrick's Zip Slip detection includes testing with crafted ZIP archives containing paths like:
../../test-payload
../windows/system32/calc.exe
/etc/passwd
/usr/bin/id
The scanner verifies whether these paths escape the intended extraction directory, which would indicate a vulnerable implementation.
For CI/CD integration, add this GitHub Action to your Feathersjs repository:
- name: Run middleBrick Security Scan
uses: middlebrick/middlebrick-action@v1
with:
target-url: http://localhost:3030
fail-on-severity: high
output-format: jsonThis automatically fails your build if Zip Slip vulnerabilities are detected in your Feathersjs API endpoints.
Feathersjs-Specific Remediation
Securely handling ZIP files in Feathersjs requires path validation and safe extraction practices. The most robust approach uses Node.js's built-in path module to validate extraction paths:
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');
class SecureFileUploadService {
async uploadZip(file) {
const zip = new AdmZip(file.buffer);
const targetDir = path.join(__dirname, '../uploads');
// Validate each entry before extraction
const zipEntries = zip.getEntries();
for (const entry of zipEntries) {
const entryPath = path.join(targetDir, entry.entryName);
// Ensure path is within target directory
if (!entryPath.startsWith(targetDir + path.sep)) {
throw new Error(`Invalid path: ${entry.entryName}`);
}
// Prevent absolute paths
if (path.isAbsolute(entry.entryName)) {
throw new Error(`Absolute paths not allowed: ${entry.entryName}`);
}
// Prevent Windows drive letters
if (entry.entryName.match(/^[A-Za-z]:/)) {
throw new Error(`Drive letters not allowed: ${entry.entryName}`);
}
}
// Safe to extract
zip.extractAllTo(targetDir, true);
}
}
module.exports = SecureFileUploadService;For Feathersjs hooks, implement validation before extraction:
const path = require('path');
const AdmZip = require('adm-zip');
const validateZipEntries = (zip, targetDir) => {
const entries = zip.getEntries();
const resolvedTarget = path.resolve(targetDir);
for (const entry of entries) {
const entryPath = path.resolve(targetDir, entry.entryName);
if (!entryPath.startsWith(resolvedTarget)) {
throw new Error(`Zip Slip attempt detected: ${entry.entryName}`);
}
}
};
module.exports = {
before: {
create: [hooks.authenticate('jwt'), validateZip]
},
async upload(context) {
const { file } = context.data;
const targetDir = path.join(__dirname, '../user-content');
const zip = new AdmZip(file.buffer);
validateZipEntries(zip, targetDir);
zip.extractAllTo(targetDir, true);
return context;
}
};Alternative safe extraction using yauzl with streaming:
const fs = require('fs');
const path = require('path');
const yauzl = require('yaulz');
async function safeExtract(zipBuffer, targetDir) {
return new Promise((resolve, reject) => {
yauzl.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => {
if (err) return reject(err);
zipfile.readEntry();
zipfile.on('entry', (entry) => {
const entryPath = path.join(targetDir, entry.fileName);
const resolvedTarget = path.resolve(targetDir);
if (!path.resolve(entryPath).startsWith(resolvedTarget)) {
zipfile.close();
return reject(new Error('Zip Slip detected'));
}
if (entry.fileName.endsWith('/')) {
// Directory
fs.mkdirSync(entryPath, { recursive: true });
zipfile.readEntry();
} else {
// File
zipfile.openReadStream(entry, (err, readStream) => {
if (err) return reject(err);
const dir = path.dirname(entryPath);
fs.mkdirSync(dir, { recursive: true });
const writeStream = fs.createWriteStream(entryPath);
readStream.pipe(writeStream);
writeStream.on('close', () => zipfile.readEntry());
});
}
});
zipfile.once('end', () => resolve());
});
});
}Additional Feathersjs-specific security measures:
- Use
multerwith file type validation to reject non-ZIP files - Implement size limits on uploaded archives
- Store extracted files in a dedicated, non-executable directory
- Set restrictive file permissions on extracted content
- Add logging for archive processing with entry validation failures
Frequently Asked Questions
How can I test if my Feathersjs API is vulnerable to Zip Slip?
../../etc/passwd. You can also manually test by creating a ZIP with ../../test.txt and attempting to upload it to your Feathersjs service. If the file appears outside your intended directory, you have a Zip Slip vulnerability.