{"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":"exception","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/9b17a8ac97b398a496b38e423ae24e433b43254f/providers/local-parser.mjs#L67-L103","documentation":"resolveInsideRoot resolves a configured path against PROJECT_ROOT via realpathSync and confirms the resolved path is either PROJECT_ROOT itself or begins with PROJECT_ROOT + path separator. If not, the path escapes the project tree and this error fires. It is the path-traversal guard for parser scripts/args — it stops '../' or symlinked paths from executing files outside the repo.","triggerScenarios":"A configured parser.script or parser path resolves outside PROJECT_ROOT — e.g. '../../etc/passwd', an absolute path like '/usr/bin/python3' passed as a script, or a symlink inside the repo that points outside. realpathSync resolves symlinks before the prefix check, so a symlinked escape is also caught.","commonSituations":"An absolute system path was given as a script; a relative path with '../' segments; a symlink under the repo pointing to an external binary; the repo was moved and PROJECT_ROOT no longer matches the script location.","solutions":["Move the parser script inside the project tree and reference it with a repo-relative path.","If you need a system interpreter, set it as parser.command (a whitelisted interpreter like python3/node), not as a script path.","Remove any '../' segments or symlinks that escape the repo.","Run realpathSync on the path manually to see where it actually resolves."],"exampleFix":"# before\nparser:\n  command: python3\n  script: /opt/parsers/acme.py\n\n# after (script moved into repo)\nparser:\n  command: python3\n  script: parsers/acme.py","handlingStrategy":"validation","validationCode":"import { realpathSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\nexport function isInsideRoot(rawPath, root) {\n  try {\n    const resolved = realpathSync(resolve(root, String(rawPath)));\n    return resolved === root || resolved.startsWith(root + sep);\n  } catch { return false; }\n}","typeGuard":"import { realpathSync } from 'node:fs';\nimport { resolve, sep } from 'node:path';\n/** @param {string} rawPath @param {string} root */\nfunction staysInRoot(rawPath, root) {\n  try {\n    const r = realpathSync(resolve(root, rawPath));\n    return r === root || r.startsWith(root + sep);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await provider.fetch(entry, ctx);\n} catch (err) {\n  if (err.message.includes('escapes the project root')) console.error(`[security] ${err.message}`);\n  throw err;\n}","preventionTips":["Keep parser scripts inside the repo; reference them with repo-relative paths.","Use a whitelisted interpreter as parser.command and an in-repo script as parser.script.","Never configure absolute system paths or '../'-laden relative paths as script paths.","Treat a path-escape error as a security signal and audit how the path was configured."],"tags":["path-traversal","security","local-parser","symlink"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}