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

  1. Replace the `opencollection.yml` symlink with a real file copy inside the archive.
  2. Re-export via Bruno rather than zipping a directory containing symlinks.
  3. 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

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


AI-assisted analysis of usebruno/bruno@9bdd81c7bd (2026-08-13). Data as JSON: /api/errors/597367981b32e8af. Report an issue: GitHub.