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

  1. Open the ZIP and replace the `bruno.json` symlink with a real file copy.
  2. Re-export the collection via Bruno's export rather than archiving a directory that contains symlinks.
  3. 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

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


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