{"record":{"id":"7b98421b4bb7582a","repo":"affaan-m/ECC","slug":"path-traversal-rejected-relpath","errorCode":null,"errorMessage":"Path traversal rejected: ${relPath}","messagePattern":"Path traversal rejected: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/hooks/observe-runner.js","lineNumber":31,"sourceCode":"    return String(options.pluginRoot).trim();\n  }\n  if (process.env.CLAUDE_PLUGIN_ROOT && process.env.CLAUDE_PLUGIN_ROOT.trim()) {\n    return process.env.CLAUDE_PLUGIN_ROOT.trim();\n  }\n  if (process.env.ECC_PLUGIN_ROOT && process.env.ECC_PLUGIN_ROOT.trim()) {\n    return process.env.ECC_PLUGIN_ROOT.trim();\n  }\n  return path.resolve(__dirname, '..', '..');\n}\n\nfunction resolveTarget(rootDir, relPath) {\n  const resolvedRoot = path.resolve(rootDir);\n  const resolvedTarget = path.resolve(rootDir, relPath);\n  if (\n    resolvedTarget !== resolvedRoot &&\n    !resolvedTarget.startsWith(resolvedRoot + path.sep)\n  ) {\n    throw new Error(`Path traversal rejected: ${relPath}`);\n  }\n  return resolvedTarget;\n}\n\nfunction toShellPath(filePath) {\n  const normalized = String(filePath || '');\n  if (process.platform !== 'win32') {\n    return normalized;\n  }\n\n  return normalized\n    .replace(/^([A-Za-z]):[\\\\/]/, (_, driveLetter) => `/${driveLetter.toLowerCase()}/`)\n    .replace(/\\\\/g, '/');\n}\n\nfunction findShellBinary() {\n  const candidates = [];\n  if (process.env.BASH && process.env.BASH.trim()) {","sourceCodeStart":13,"sourceCodeEnd":49,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/hooks/observe-runner.js#L13-L49","documentation":"observe-runner.js resolves a relative path against the plugin root and confirms the resolved target stays inside that root (allowing the root itself). If relPath escapes via '..' segments or an absolute override, the guard throws to block path traversal. This protects the observe.sh hook launcher from being redirected outside the ECC plugin tree.","triggerScenarios":"A hook payload or environment variable sets the observe.sh relative path to something like ../../etc/passwd, /etc/shadow, or any value whose resolved absolute form is not under the plugin root.","commonSituations":"CLAUDE_PLUGIN_ROOT or ECC_PLUGIN_ROOT overridden to point outside the installed plugin, a symlink inside the tree that resolves elsewhere, or tampered/malformed hook input from a compromised workspace.","solutions":["Ensure CLAUDE_PLUGIN_ROOT / ECC_PLUGIN_ROOT point at the real ECC plugin directory","Do not override the observe.sh relative path used by the hook","Reinstall ECC if the plugin files were moved or the tree is corrupted"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const path = require('path');\nfunction assertWithinRoot(rootDir, relPath) {\n  const root = path.resolve(rootDir);\n  const target = path.resolve(rootDir, relPath);\n  if (target !== root && !target.startsWith(root + path.sep)) {\n    throw new Error(`Path traversal rejected (pre-check): ${relPath}`);\n  }\n  return target;\n}","typeGuard":"function isPathInsideRoot(rootDir, relPath) {\n  const root = path.resolve(rootDir);\n  const target = path.resolve(rootDir, relPath);\n  return target === root || target.startsWith(root + path.sep);\n}","tryCatchPattern":"try { resolveTarget(root, rel); }\ncatch (err) {\n  if (/Path traversal rejected/.test(err.message)) {\n    console.error(`Refusing to resolve ${rel} outside plugin root ${root}`);\n    process.exit(126);\n  }\n  throw err;\n}","preventionTips":["Never override CLAUDE_PLUGIN_ROOT / ECC_PLUGIN_ROOT to point outside the installed plugin","Treat any hook input referencing '..' or absolute paths as untrusted","Reinstall ECC if the plugin tree has been relocated"],"tags":["security","path-traversal","hooks","observe-runner"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}