{"record":{"id":"7b98421b4bb7582a","repo":"affaan-m/ECC","slug":"path-traversal-rejected-relpath","errorCode":null,"errorMessage":"Path traversal rejected: ${relPath}","messagePattern":"Path traversal rejected: (.+?)","errorType":"exception","errorClass":"Error","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/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/hooks/observe-runner.js#L13-L49","documentation":"resolveTarget in scripts/hooks/observe-runner.js resolves a relative path against a root directory and rejects any target that escapes the root. This is a security guard preventing path traversal (e.g. `../`) from making the hook act on files outside the intended directory.","triggerScenarios":"Passing a relPath like `../../etc/passwd`, an absolute path outside rootDir, or a path whose canonical (symlink/..-resolved) form lands outside the root.","commonSituations":"Hook configuration referencing files outside the project, user-supplied paths in hook payloads, symlinks resolving outside the project after path.resolve canonicalization.","solutions":["Ensure the target path is relative to and inside the hook's root directory","Remove `..` segments or leading absolute paths from the configured path","If the file legitimately lives elsewhere, move it into the project root or update rootDir","Verify symlink resolution — the canonical location must be inside root"],"exampleFix":"// before\nresolveTarget(projectRoot, '../../shared/config.json')\n// after\nresolveTarget(projectRoot, 'config/shared.json')","handlingStrategy":"validation","validationCode":"const path = require('path');\nfunction isInsideRoot(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}\n// call resolveTarget only if isInsideRoot returns true","typeGuard":"const isSafeRelPath = (p) => typeof p === 'string' && p.length > 0 && !p.includes('..') && !path.isAbsolute(p);","tryCatchPattern":"try {\n  const target = resolveTarget(rootDir, relPath);\n} catch (err) {\n  if (String(err.message).startsWith('Path traversal rejected:')) {\n    console.error(`Refusing out-of-root path: ${relPath}`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Store root-relative paths only in hook configuration","Never forward raw user input as a file path to hooks","Re-check paths after symlinks change; resolve() canonicalizes","Add unit tests asserting traversal attempts throw"],"tags":["security","path-traversal","hooks"],"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"}