usebruno/bruno · error · Error
Security error: bruno.json cannot be a symbolic link
Error message
Security error: bruno.json cannot be a symbolic link
What it means
Security guard in the ZIP import path. Once `bruno.json` is found in the extracted directory, `fs.lstatSync(...).isSymbolicLink()` is checked. A symlinked config could redirect outside the temp extraction dir (Zip-Slip / symlink traversal), so Bruno refuses to load it.
Source
Thrown at packages/bruno-electron/src/ipc/collection.js:2728
if (extractedItems.length === 1) {
const singleItem = path.join(tempDir, extractedItems[0]);
const singleItemStat = fs.lstatSync(singleItem);
if (singleItemStat.isDirectory() && !singleItemStat.isSymbolicLink()) {
collectionDir = singleItem;
}
}
const brunoJsonPath = path.join(collectionDir, 'bruno.json');
const openCollectionYmlPath = path.join(collectionDir, 'opencollection.yml');
if (!fs.existsSync(brunoJsonPath) && !fs.existsSync(openCollectionYmlPath)) {
throw new Error('Invalid collection: Neither bruno.json nor opencollection.yml found in the ZIP file');
}
// Ensure config files are not symlinks
if (fs.existsSync(brunoJsonPath) && fs.lstatSync(brunoJsonPath).isSymbolicLink()) {
throw new Error('Security error: bruno.json cannot be a symbolic link');
}
if (fs.existsSync(openCollectionYmlPath) && fs.lstatSync(openCollectionYmlPath).isSymbolicLink()) {
throw new Error('Security error: opencollection.yml cannot be a symbolic link');
}
let collectionName = 'Imported Collection';
let brunoConfig = { name: collectionName, version: '1', type: 'collection', ignore: ['node_modules', '.git'] };
if (fs.existsSync(openCollectionYmlPath)) {
try {
const content = fs.readFileSync(openCollectionYmlPath, 'utf8');
const parsed = parseCollection(content, { format: 'yml' });
brunoConfig = parsed.brunoConfig || brunoConfig;
collectionName = brunoConfig.name || collectionName;
} catch (e) {
console.error(`Error parsing opencollection.yml at ${openCollectionYmlPath}:`, e);
}
} else if (fs.existsSync(brunoJsonPath)) {
try {View on GitHub (pinned to 9bdd81c7bd)
Solutions
- Open the ZIP and replace the `bruno.json` symlink with a real file copy.
- Re-export the collection via Bruno's export rather than archiving a directory that contains symlinks.
- If the ZIP came from an untrusted source, audit the symlink target and treat it as a potential attack.
Example fix
// before (on disk before zipping) ln -s /shared/bruno.json my-collection/bruno.json // after: copy the real file in cp /shared/bruno.json my-collection/bruno.json
Defensive patterns
Strategy: validation
Validate before calling
const assertRealConfigFile = (p) => {
const fs = require('fs');
if (fs.existsSync(p) && fs.lstatSync(p).isSymbolicLink()) {
throw new Error(`Refusing symlink config: ${p}`);
}
}; Prevention
- Never archive directories containing symlinks to config files.
- Treat collection ZIPs from untrusted sources as hostile - inspect entries first.
- Prefer Bruno's native export over manual zipping.
When it happens
Trigger: A crafted (or sloppy) ZIP whose `bruno.json` entry is a symlink pointing to an absolute path or a `../`-traversing relative target; a collection assembled on Unix with `ln -s` and then archived.
Common situations: A malicious collection shared from an untrusted source; a developer symlinking a shared config into several collection dirs and zipping the result; CI that symlinks generated configs.
Related errors
- Security error: opencollection.yml cannot be a symbolic link
- Security error: Symlink "${entry.name}" points outside extra
- Import collection failed
- The Collection file is corrupted
- Import collection failed: ${err.message}
AI-assisted analysis of usebruno/bruno@9bdd81c7bd (2026-08-13).
Data as JSON: /api/errors/3a77c893a1b7e55d.
Report an issue: GitHub.