Zip Slip in Express with Firestore
Zip Slip in Express with Firestore — how this specific combination creates or exposes the vulnerability
Zip Slip is a path traversal vulnerability that occurs when an application constructs file paths using user-supplied input without proper validation. In an Express application that interacts with Google Cloud Firestore, the risk emerges not from Firestore itself, but from how the application handles file uploads, exports, or backup imports where a filename is used to build a local filesystem path before or after Firestore operations.
Consider an endpoint that downloads a document template stored in Firestore and writes it to disk for user download. If the document name or a provided path parameter is used directly with path.join() or string concatenation to form a filesystem path, an attacker can supply sequences like ../../../etc/passwd. Even when Firestore data is validated, the application may still resolve the constructed path outside the intended directory, leading to unauthorized file access or overwrite. Firestore rules may correctly limit read access, but the server-side path resolution bypasses those rules because the file operation occurs on the host running Express, not in Firestore storage.
Another scenario involves importing data into Firestore from user-provided archives. If an archive contains entries with crafted paths (e.g., ../../malicious.json) and the extraction routine uses unsafe path joining, files can be written outside the target directory. If the imported files are later loaded into Firestore without sanitization, the malicious content may be stored, but the immediate danger is filesystem compromise on the host. Because Firestore operations are authenticated and rules-based, developers may mistakenly assume that all data handling is safe, overlooking insecure local file interactions triggered by user-controlled input.
The combination of Express routing, dynamic path construction, and Firestore integration amplifies the impact when logging, backups, or temporary files are involved. An attacker who can manipulate path inputs may read sensitive server files or overwrite critical application files, even when Firestore access is properly restricted. This highlights the need to validate and sanitize any path derived from user input before using it in filesystem operations, regardless of how securely Firestore data itself is managed.
Firestore-Specific Remediation in Express — concrete code fixes
To prevent Zip Slip in an Express application that uses Firestore, always resolve and sanitize file paths on the server before any filesystem operation. Use a strict base directory and validate that the resulting path remains within that directory. Below are concrete, secure code examples demonstrating safe handling when integrating Firestore with file operations in Express.
Secure Path Resolution for File Downloads
When generating a downloadable file based on a Firestore document, avoid using raw user input in path construction. Instead, use a controlled filename mapping and path.resolve() with a defined base directory.
const express = require('express');
const { initializeApp } = require('firebase-admin');
const path = require('path');
const fs = require('fs');
const app = express();
const db = initializeApp().firestore();
app.get('/templates/:docId/download', async (req, res) => {
const { docId } = req.params;
// Fetch document metadata from Firestore (rules-enforced)
const doc = await db.collection('templates').doc(docId).get();
if (!doc.exists) {
return res.status(404).send('Template not found');
}
const data = doc.data();
const safeName = data.filename || 'document.pdf'; // Assume filename is vetted on write
const baseDir = path.resolve(__dirname, 'safe-templates');
const filePath = path.resolve(baseDir, safeName);
// Ensure the resolved path is within baseDir
if (!filePath.startsWith(baseDir)) {
return res.status(400).send('Invalid file path');
}
res.download(filePath, safeName, (err) => {
if (err) res.status(500).send('Download error');
});
});
Validating Archive Imports to Avoid Traversal
When importing files into Firestore from uploaded archives (e.g., ZIP), inspect each entry and sanitize paths explicitly. Use a library like adm-zip and reject entries containing path traversal sequences.
const AdmZip = require('adm-zip');
const { initializeApp } = require('firebase-admin');
const path = require('path');
const db = initializeApp().firestore();
app.post('/import', async (req, res) => {
const zip = new AdmZip(req.file.buffer);
const baseDir = path.resolve(__dirname, 'imports');
for (const entry of zip.getEntries()) {
const normalized = path.normalize(entry.entryName).replace(/^(\/|\\)/, '');
const resolved = path.resolve(baseDir, normalized);
if (!resolved.startsWith(baseDir)) {
return res.status(400).send('Invalid archive entry: ' + entry.entryName);
}
// Extract safely and optionally store metadata in Firestore
// fs.mkdirSync(path.dirname(resolved), { recursive: true });
// fs.writeFileSync(resolved, entry.getData());
// await db.collection('imports').add({ name: entry.entryName, path: resolved });
}
res.send('Import validated and ready to process');
});
These patterns ensure that user-controlled input never dictates filesystem locations directly. Combine this with Firestore security rules that limit read/write access to trusted sources, and you reduce the attack surface across both the application and the database layer.