usebruno/bruno · critical · Error
Security error: Symlink "${entry.name}" points outside extra
Error message
Security error: Symlink "${entry.name}" points outside extraction directory What it means
Security guard against zip-slip via symlinks. After extractZip runs, validateNoExternalSymlinks walks the extracted tree; for each symlink it resolves the target with path.resolve and verifies the result is inside baseDir (or equals it). A symlink that escapes triggers this throw, aborting the import.
Source
Thrown at packages/bruno-electron/src/ipc/collection.js:2693
if (!collectionLocation || !fs.existsSync(collectionLocation)) {
throw new Error('Collection location does not exist');
}
const tempDir = path.join(os.tmpdir(), `bruno_zip_import_${Date.now()}`);
await fsExtra.ensureDir(tempDir);
// Validates that no symlinks point outside the base directory
const validateNoExternalSymlinks = (dir, baseDir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const stat = fs.lstatSync(fullPath);
if (stat.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(fullPath);
const resolvedTarget = path.resolve(path.dirname(fullPath), linkTarget);
if (!resolvedTarget.startsWith(baseDir + path.sep) && resolvedTarget !== baseDir) {
throw new Error(`Security error: Symlink "${entry.name}" points outside extraction directory`);
}
}
if (stat.isDirectory() && !stat.isSymbolicLink()) {
validateNoExternalSymlinks(fullPath, baseDir);
}
}
};
try {
await extractZip(zipFilePath, { dir: tempDir });
validateNoExternalSymlinks(tempDir, tempDir);
const extractedItems = fs.readdirSync(tempDir);
let collectionDir = tempDir;
if (extractedItems.length === 1) {View on GitHub (pinned to 9bdd81c7bd)
Solutions
- Refuse to import the offending zip — treat as untrusted.
- Pre-scan the zip's central directory for symlink entries and reject before extraction.
- Use a quarantine-aware extractor that strips or rewrites escaping symlinks.
- On detection, clean up tempDir before re-prompting the user.
Defensive patterns
Strategy: try-catch
Validate before calling
const yauzl = require('yauzl');
// pre-scan zip central directory for symlink entries that resolve outside base
async function zipHasExternalSymlinks(zipPath) {
return await new Promise((resolve) => {
yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => {
if (err) return resolve(false);
zip.on('entry', (e) => resolve(/ symlink$/i.test(e.externalFileAttributes.toString(16))));
zip.on('end', () => resolve(false));
zip.readEntry();
});
});
}
if (await zipHasExternalSymlinks(zipFilePath)) {
throw new Error('Refusing to import: zip contains escaping symlinks');
} Try / catch
try {
await ipcRenderer.invoke('renderer:import-collection-zip', zipFilePath, collectionLocation);
} catch (err) {
if (/Security error: Symlink/.test(err.message)) {
// treat zip as untrusted; do NOT retry. Quarantine or discard.
await cleanupTemp();
surfaceSecurityWarningToUser(err.message);
} else {
throw err;
}
} Prevention
- Treat imported zips as untrusted; pre-scan the central directory for symlink entries before extraction.
- Prefer extractors that strip or refuse symlinks by default.
- On detection, clean up the extraction tempDir and log the event for security audit.
- Never relax the baseDir containment check — it is the load-bearing zip-slip defense.
When it happens
Trigger: The imported zip contains a symbolic link whose target resolves to a path outside the extraction tempDir (e.g. linking to /etc/passwd, an absolute path, or a `../` chain that escapes).
Common situations: Malicious zip crafted for zip-slip; zip produced by a tool that materializes absolute symlinks; intentionally relative symlinks whose resolution crosses the base.
Related errors
- Invalid file name
- Security error: bruno.json cannot be a symbolic link
- Security error: opencollection.yml cannot be a symbolic link
- Could not determine collection for target directory
- Invalid .env filename
AI-assisted analysis of usebruno/bruno@9bdd81c7bd (2026-08-13).
Data as JSON: /api/errors/12a2cb76727fb52b.
Report an issue: GitHub.