usebruno/bruno · error · Error
Security error: opencollection.yml cannot be a symbolic link
Error message
Security error: opencollection.yml cannot be a symbolic link
What it means
Same security guard as the bruno.json check, applied to `opencollection.yml`. After the file is found, `fs.lstatSync(...).isSymbolicLink()` rejects a symlinked config to prevent traversal outside the extraction directory during ZIP import.
Source
Thrown at packages/bruno-electron/src/ipc/collection.js:2731
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 {
brunoConfig = JSON.parse(fs.readFileSync(brunoJsonPath, 'utf8'));
collectionName = brunoConfig.name || collectionName;
} catch (e) {View on GitHub (pinned to 9bdd81c7bd)
Solutions
- Replace the `opencollection.yml` symlink with a real file copy inside the archive.
- Re-export via Bruno rather than zipping a directory containing symlinks.
- Treat archives from untrusted sources as hostile and inspect entries first.
Example fix
// before ln -s /shared/opencollection.yml my-collection/opencollection.yml // after cp /shared/opencollection.yml my-collection/opencollection.yml
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.
- Inspect opencollection.yml entries in untrusted archives before importing.
- Re-export via Bruno instead of zipping symlinked configs.
When it happens
Trigger: A ZIP whose `opencollection.yml` entry is a symlink to an absolute or `../`-traversing target; a collection directory where opencollection.yml was symlinked and then archived.
Common situations: Untrusted collection archive crafted to escape the temp dir; a shared config symlinked across multiple collections and zipped.
Related errors
- Security error: bruno.json 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/597367981b32e8af.
Report an issue: GitHub.