{"record":{"id":"00897c415f79e122","repo":"santifer/career-ops","slug":"local-parser-path-escapes-the-project-root-raw","errorCode":null,"errorMessage":"local-parser: path escapes the project root: ${rawPath}","messagePattern":"local-parser: path escapes the project root: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"providers/local-parser.mjs","lineNumber":85,"sourceCode":"\n  return scriptArg ? expandParserArg(scriptArg, entry) : null;\n}\n\nfunction buildParserArgs(entry) {\n  const parser = entry.parser || {};\n  const args = [];\n\n  if (parser.script) args.push(parser.script);\n  if (Array.isArray(parser.args)) args.push(...parser.args);\n\n  return args.map(arg => expandParserArg(arg, entry));\n}\n\n// Resolve a configured path and confirm it stays inside the project tree.\nfunction resolveInsideRoot(rawPath) {\n  const resolved = realpathSync(resolve(PROJECT_ROOT, String(rawPath)));\n  if (resolved !== PROJECT_ROOT && !resolved.startsWith(PROJECT_ROOT + sep)) {\n    throw new Error(`local-parser: path escapes the project root: ${rawPath}`);\n  }\n  return resolved;\n}\n\n// The command is either a whitelisted interpreter (resolved via PATH) or a script\n// that lives inside the repo. Anything else is rejected.\nfunction resolveCommand(command) {\n  const value = String(command || '');\n  if (!value) throw new Error('local-parser: parser.command is required');\n  if (!value.includes('/') && ALLOWED_INTERPRETERS.has(value)) return value;\n  return resolveInsideRoot(value);\n}\n\n// Validate the whole invocation and return what to spawn. Throws on anything unsafe.\nfunction resolveInvocation(entry) {\n  const rawCommand = String(entry.parser?.command || '');\n  const command = resolveCommand(rawCommand);\n  const args = buildParserArgs(entry);","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/providers/local-parser.mjs#L67-L103","documentation":"resolveInsideRoot() resolves a configured path against PROJECT_ROOT and follows symlinks via realpathSync, then verifies the result is still inside the project tree. This blocks path traversal (../) and symlink escapes so portals.yml can never point the local parser at arbitrary files outside the repo.","triggerScenarios":"parser.command or parser.script resolving outside PROJECT_ROOT — e.g. script: ../../etc/passwd, an absolute path like /usr/bin/curl, or an in-repo path whose symlink target leaves the repo.","commonSituations":"Absolute paths in config that assume a different install location; ../../../ traversal in a copied template entry; a symlink inside the repo pointing to a user home directory; moving the repo so previously valid relative paths now resolve elsewhere.","solutions":["Place the script inside the project and use a repo-relative path, e.g. parsers/my-parser.py.","Remove ../ segments and any absolute path from parser.command / parser.script / detected script args.","Check for symlinks: the check uses realpathSync, so retarget any in-repo symlink to a location inside the repo.","If the tool must run an external binary, add it to ALLOWED_INTERPRETERS in providers/local-parser.mjs rather than pointing at it by path."],"exampleFix":"// before (portals.yml)\nparser: {command: python3, args: [\"../shared/parse_jobs.py\"]}\n// after\nparser: {command: python3, args: [\"parsers/parse_jobs.py\"]}","handlingStrategy":"validation","validationCode":"import { realpathSync } from 'fs';\nimport { resolve, sep } from 'path';\nconst ROOT = realpathSync(process.cwd());\nfunction assertInsideRoot(p) {\n  const r = realpathSync(resolve(ROOT, String(p)));\n  if (r !== ROOT && !r.startsWith(ROOT + sep)) throw new Error(`path escapes project root: ${p}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await localParser.fetch(entry);\n} catch (e) {\n  if (String(e.message).includes('escapes the project root')) {\n    console.error(`${entry.name}: move the script into the repo and use a relative path`);\n    return [];\n  }\n  throw e;\n}","preventionTips":["Use repo-relative paths for parser.command/script; never absolute paths or ../.","Audit in-repo symlinks so realpath stays inside the project.","Keep shared helper scripts in the repo (e.g. parsers/) instead of referencing external locations.","Re-check paths after moving or cloning the repo to a new location."],"tags":["security","path-traversal","config"],"backgroundTag":"path-traversal-blocked","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}