{"record":{"id":"583e0f25377b120c","repo":"affaan-m/ECC","slug":"output-destination-must-not-be-a-symlink","errorCode":null,"errorMessage":"output destination must not be a symlink","messagePattern":"output destination must not be a symlink","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"skills/master-agreement-generator/scripts/build-agreement.js","lineNumber":155,"sourceCode":"\nfunction outputPaths(outDir, file) {\n  const root = path.resolve(outDir);\n  const destinations = ['md', 'docx'].map(extension => path.resolve(root, `${file} MASTER.${extension}`));\n  for (const destination of destinations) {\n    if (path.dirname(destination) !== root) {\n      throw new Error('spec.file must keep generated files directly inside the output directory');\n    }\n    // lstat also detects dangling links. Check BOTH outputs before the first write,\n    // even when conversion is disabled. The caller must control this directory;\n    // these checks do not isolate concurrent hostile filesystem changes.\n    let stat;\n    try {\n      stat = fs.lstatSync(destination);\n    } catch (error) {\n      if (error.code !== 'ENOENT') throw error;\n    }\n    if (stat?.isSymbolicLink()) {\n      throw new Error('output destination must not be a symlink');\n    }\n  }\n  return { root, mdPath: destinations[0], docxPath: destinations[1] };\n}\n\nfunction build(templatePath, specPath, outDir, options = {}) {\n  const template = fs.readFileSync(templatePath, 'utf8');\n  const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));\n  const markdown = render(template, spec, options.now);\n  const { root, mdPath, docxPath } = outputPaths(outDir, spec.file);\n  fs.mkdirSync(root, { recursive: true });\n  fs.writeFileSync(mdPath, markdown, 'utf8');\n\n  // Generated DOCX is replaceable output. Never leave a stale or partial copy\n  // beside a newly built Markdown draft, including explicit Markdown-only builds.\n  fs.rmSync(docxPath, { force: true });\n  const result = { markdown: mdPath, docx: null, docxSkipped: false, documentStatus: 'draft' };\n  if (options.markdownOnly === true) {","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/master-agreement-generator/scripts/build-agreement.js#L137-L173","documentation":"Before writing, outputPaths() lstat's each destination ('<file> MASTER.md' and '<file> MASTER.docx'). If either exists and is a symbolic link, it throws this error and aborts before any write. This prevents a pre-planted symlink in the output directory from redirecting the generated agreement writes to an arbitrary target.","triggerScenarios":"A symlink named e.g. 'NDA MASTER.docx' already exists in outDir (planted by an attacker or left over from an earlier manual ln -s), and build() runs with that outDir.","commonSituations":"Shared/temp output directories where another process created symlinks; developer experimentation with ln -s pointing outputs elsewhere; CI caches that restored symlinked artifacts.","solutions":["Remove the symlink from the output directory (ls -l outDir to find it, rm the link), then rerun the build.","Use a clean, dedicated output directory that only the build controls.","If a symlink is intentional, delete it and let the script create the real file; the library will never write through links.","Note the source comment: these checks do not defend against concurrent hostile filesystem changes — keep outDir private during the run."],"exampleFix":"// before\n$ ln -s /etc/passwd out/NDA\\ MASTER.docx\n$ node build-agreement.js ... # throws: output destination must not be a symlink\n// after\n$ rm out/NDA\\ MASTER.docx\n$ node build-agreement.js ... # succeeds","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nfunction ensureNoSymlinks(outDir, file) {\n  for (const ext of ['md', 'docx']) {\n    const p = `${outDir}/${file} MASTER.${ext}`;\n    try {\n      if (fs.lstatSync(p).isSymbolicLink()) throw new Error(`symlink at ${p}`);\n    } catch (e) { if (e.code !== 'ENOENT') throw e; }\n  }\n}\nensureNoSymlinks(outDir, spec.file);","typeGuard":"null","tryCatchPattern":"try {\n  outputPaths(outDir, spec.file);\n} catch (e) {\n  if (e.message === 'output destination must not be a symlink') {\n    console.error('Remove pre-existing symlinks from the output directory before building');\n  } else throw e;\n}","preventionTips":["Use a dedicated, private output directory owned by the build process.","Audit output directories for unexpected symlinks (find outDir -type l), especially in shared/CI caches.","Don't pre-create output files as links; let the script create regular files."],"tags":["security","symlink","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}