{"record":{"id":"3a77c893a1b7e55d","repo":"usebruno/bruno","slug":"security-error-bruno-json-cannot-be-a-symbolic-li","errorCode":null,"errorMessage":"Security error: bruno.json cannot be a symbolic link","messagePattern":"Security error: bruno\\.json cannot be a symbolic link","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/bruno-electron/src/ipc/collection.js","lineNumber":2728,"sourceCode":"\n        if (extractedItems.length === 1) {\n          const singleItem = path.join(tempDir, extractedItems[0]);\n          const singleItemStat = fs.lstatSync(singleItem);\n          if (singleItemStat.isDirectory() && !singleItemStat.isSymbolicLink()) {\n            collectionDir = singleItem;\n          }\n        }\n\n        const brunoJsonPath = path.join(collectionDir, 'bruno.json');\n        const openCollectionYmlPath = path.join(collectionDir, 'opencollection.yml');\n\n        if (!fs.existsSync(brunoJsonPath) && !fs.existsSync(openCollectionYmlPath)) {\n          throw new Error('Invalid collection: Neither bruno.json nor opencollection.yml found in the ZIP file');\n        }\n\n        // Ensure config files are not symlinks\n        if (fs.existsSync(brunoJsonPath) && fs.lstatSync(brunoJsonPath).isSymbolicLink()) {\n          throw new Error('Security error: bruno.json cannot be a symbolic link');\n        }\n        if (fs.existsSync(openCollectionYmlPath) && fs.lstatSync(openCollectionYmlPath).isSymbolicLink()) {\n          throw new Error('Security error: opencollection.yml cannot be a symbolic link');\n        }\n\n        let collectionName = 'Imported Collection';\n        let brunoConfig = { name: collectionName, version: '1', type: 'collection', ignore: ['node_modules', '.git'] };\n        if (fs.existsSync(openCollectionYmlPath)) {\n          try {\n            const content = fs.readFileSync(openCollectionYmlPath, 'utf8');\n            const parsed = parseCollection(content, { format: 'yml' });\n            brunoConfig = parsed.brunoConfig || brunoConfig;\n            collectionName = brunoConfig.name || collectionName;\n          } catch (e) {\n            console.error(`Error parsing opencollection.yml at ${openCollectionYmlPath}:`, e);\n          }\n        } else if (fs.existsSync(brunoJsonPath)) {\n          try {","sourceCodeStart":2710,"sourceCodeEnd":2746,"githubUrl":"https://github.com/usebruno/bruno/blob/9bdd81c7bdc57006e5f5ebffb79321a8d979f712/packages/bruno-electron/src/ipc/collection.js#L2710-L2746","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Open the ZIP and replace the `bruno.json` symlink with a real file copy.","Re-export the collection via Bruno's export rather than archiving a directory that contains symlinks.","If the ZIP came from an untrusted source, audit the symlink target and treat it as a potential attack."],"exampleFix":"// before (on disk before zipping)\nln -s /shared/bruno.json my-collection/bruno.json\n// after: copy the real file in\ncp /shared/bruno.json my-collection/bruno.json","handlingStrategy":"validation","validationCode":"const assertRealConfigFile = (p) => {\n  const fs = require('fs');\n  if (fs.existsSync(p) && fs.lstatSync(p).isSymbolicLink()) {\n    throw new Error(`Refusing symlink config: ${p}`);\n  }\n};","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never archive directories containing symlinks to config files.","Treat collection ZIPs from untrusted sources as hostile - inspect entries first.","Prefer Bruno's native export over manual zipping."],"tags":["security","symlink","collection","import"],"backgroundTag":null,"analyzedSha":"9bdd81c7bdc57006e5f5ebffb79321a8d979f712","analyzedAt":"2026-08-13T04:09:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}