{"record":{"id":"5b70c3f16a417dd8","repo":"affaan-m/ECC","slug":"spec-file-must-keep-generated-files-directly-inside-the","errorCode":null,"errorMessage":"spec.file must keep generated files directly inside the output directory","messagePattern":"spec\\.file must keep generated files directly inside the output directory","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"skills/master-agreement-generator/scripts/build-agreement.js","lineNumber":143,"sourceCode":"  }\n  const leftover = output.match(/\\{\\{[A-Z_]+\\}\\}/g);\n  if (leftover) {\n    throw new Error(`template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}`);\n  }\n  return `${DRAFT_NOTICE}\\n\\n${output}`;\n}\n\nfunction pandocAvailable() {\n  const probe = spawnSync('pandoc', ['--version'], CONVERTER_OPTIONS);\n  return !probe.error && probe.status === 0;\n}\n\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 = {}) {","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/master-agreement-generator/scripts/build-agreement.js#L125-L161","documentation":"outputPaths() resolves '<file> MASTER.md' and '<file> MASTER.docx' against the resolved output directory and refuses to continue if either destination resolves outside that directory (path.dirname(destination) !== root). This guards against path traversal when spec.file smuggles path components (e.g. '../..' or absolute paths) into the generated filenames.","triggerScenarios":"Calling outputPaths() (via the build pipeline) with a file value that still resolves oddly — e.g. Windows-style 'C:\\evil' names or names that expand with '..' — making the md/docx destination's dirname differ from the resolved outDir.","commonSituations":"A spec.file like '..\\..\\Users\\x\\evil' on a Windows host or a POSIX host accepting Windows-style input; a symlinked or oddly-mounted outDir where path.resolve lands somewhere unexpected.","solutions":["Pass a bare, single-component filename in spec.file (no slashes, backslashes, or '..').","Ensure outDir is an existing plain directory, not a symlink chain, so path.resolve(outDir) is the true target.","If you must write to a subdirectory, create it explicitly and pass that directory as outDir instead of encoding it in file.","Keep the built-in filename validation (error 722) intact — it prevents most cases that reach this guard."],"exampleFix":"// before\noutputPaths(outDir, '../../etc/evil');\n// after\noutputPaths(outDir, 'evil'); // writes <outDir>/evil MASTER.md|.docx","handlingStrategy":"validation","validationCode":"const path = require('path');\nfunction staysInside(outDir, file) {\n  const root = path.resolve(outDir);\n  return ['md', 'docx'].every(ext =>\n    path.dirname(path.resolve(root, `${file} MASTER.${ext}`)) === root);\n}\nif (!staysInside(outDir, spec.file)) throw new Error('file escapes output directory');","typeGuard":"null","tryCatchPattern":"try {\n  outputPaths(outDir, spec.file);\n} catch (e) {\n  if (e.message.includes('directly inside the output directory')) {\n    console.error('spec.file must be a bare filename, not a path');\n  } else throw e;\n}","preventionTips":["Never interpolate user input into output file paths without basename() + re-validation.","Keep outDir as a plain directory (not a symlink chain) so path.resolve is predictable.","Rely on the library's own filename checks (error 722) rather than bypassing them with raw values."],"tags":["security","path-traversal","validation"],"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"}