{"record":{"id":"12a2cb76727fb52b","repo":"usebruno/bruno","slug":"security-error-symlink-entry-name-points-out","errorCode":null,"errorMessage":"Security error: Symlink \"${entry.name}\" points outside extraction directory","messagePattern":"Security error: Symlink \"(.+?)\" points outside extraction directory","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/bruno-electron/src/ipc/collection.js","lineNumber":2693,"sourceCode":"      if (!collectionLocation || !fs.existsSync(collectionLocation)) {\n        throw new Error('Collection location does not exist');\n      }\n\n      const tempDir = path.join(os.tmpdir(), `bruno_zip_import_${Date.now()}`);\n      await fsExtra.ensureDir(tempDir);\n\n      // Validates that no symlinks point outside the base directory\n      const validateNoExternalSymlinks = (dir, baseDir) => {\n        const entries = fs.readdirSync(dir, { withFileTypes: true });\n        for (const entry of entries) {\n          const fullPath = path.join(dir, entry.name);\n          const stat = fs.lstatSync(fullPath);\n\n          if (stat.isSymbolicLink()) {\n            const linkTarget = fs.readlinkSync(fullPath);\n            const resolvedTarget = path.resolve(path.dirname(fullPath), linkTarget);\n            if (!resolvedTarget.startsWith(baseDir + path.sep) && resolvedTarget !== baseDir) {\n              throw new Error(`Security error: Symlink \"${entry.name}\" points outside extraction directory`);\n            }\n          }\n\n          if (stat.isDirectory() && !stat.isSymbolicLink()) {\n            validateNoExternalSymlinks(fullPath, baseDir);\n          }\n        }\n      };\n\n      try {\n        await extractZip(zipFilePath, { dir: tempDir });\n\n        validateNoExternalSymlinks(tempDir, tempDir);\n\n        const extractedItems = fs.readdirSync(tempDir);\n        let collectionDir = tempDir;\n\n        if (extractedItems.length === 1) {","sourceCodeStart":2675,"sourceCodeEnd":2711,"githubUrl":"https://github.com/usebruno/bruno/blob/9bdd81c7bdc57006e5f5ebffb79321a8d979f712/packages/bruno-electron/src/ipc/collection.js#L2675-L2711","documentation":"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.","triggerScenarios":"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).","commonSituations":"Malicious zip crafted for zip-slip; zip produced by a tool that materializes absolute symlinks; intentionally relative symlinks whose resolution crosses the base.","solutions":["Refuse to import the offending zip — treat as untrusted.","Pre-scan the zip's central directory for symlink entries and reject before extraction.","Use a quarantine-aware extractor that strips or rewrites escaping symlinks.","On detection, clean up tempDir before re-prompting the user."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const yauzl = require('yauzl');\n// pre-scan zip central directory for symlink entries that resolve outside base\nasync function zipHasExternalSymlinks(zipPath) {\n  return await new Promise((resolve) => {\n    yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => {\n      if (err) return resolve(false);\n      zip.on('entry', (e) => resolve(/ symlink$/i.test(e.externalFileAttributes.toString(16))));\n      zip.on('end', () => resolve(false));\n      zip.readEntry();\n    });\n  });\n}\nif (await zipHasExternalSymlinks(zipFilePath)) {\n  throw new Error('Refusing to import: zip contains escaping symlinks');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await ipcRenderer.invoke('renderer:import-collection-zip', zipFilePath, collectionLocation);\n} catch (err) {\n  if (/Security error: Symlink/.test(err.message)) {\n    // treat zip as untrusted; do NOT retry. Quarantine or discard.\n    await cleanupTemp();\n    surfaceSecurityWarningToUser(err.message);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Treat imported zips as untrusted; pre-scan the central directory for symlink entries before extraction.","Prefer extractors that strip or refuse symlinks by default.","On detection, clean up the extraction tempDir and log the event for security audit.","Never relax the baseDir containment check — it is the load-bearing zip-slip defense."],"tags":["security","zip-slip","symlink","ipc","import","path-traversal"],"backgroundTag":null,"analyzedSha":"9bdd81c7bdc57006e5f5ebffb79321a8d979f712","analyzedAt":"2026-08-13T04:09:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}