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

  1. Refuse to import the offending zip — treat as untrusted.
  2. Pre-scan the zip's central directory for symlink entries and reject before extraction.
  3. Use a quarantine-aware extractor that strips or rewrites escaping symlinks.
  4. 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

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


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