{"record":{"id":"73c7eb6dc896869f","repo":"affaan-m/ECC","slug":"path-traversal-rejected-relpath-73c7eb","errorCode":null,"errorMessage":"Path traversal rejected: ${relPath}","messagePattern":"Path traversal rejected: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/hooks/plugin-hook-bootstrap.js","lineNumber":102,"sourceCode":"  }\n\n  const match = rootDir.match(/^\\/([a-zA-Z])(?:\\/(.*))?$/);\n  if (!match) {\n    return rootDir;\n  }\n\n  const [, driveLetter, rest = ''] = match;\n  return `${driveLetter.toUpperCase()}:/${rest}`;\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\nlet _cachedShell = undefined;\nlet _cachedBash = undefined;\n\nfunction isPowerShellBin(bin) {\n  const base = path.basename(bin).toLowerCase();\n  return base === 'pwsh.exe' || base === 'pwsh' || base === 'powershell.exe' || base === 'powershell';\n}\n\nfunction findShellBinary() {\n  if (_cachedShell !== undefined) return _cachedShell;\n\n  const candidates = [];\n\n  // Explicit override always wins — check before any platform probing.","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/hooks/plugin-hook-bootstrap.js#L84-L120","documentation":"Identical traversal guard to observe-runner.js but in scripts/hooks/plugin-hook-bootstrap.js: it resolves the requested path against the plugin root and throws if the resolved target is neither the root itself nor a descendant. Prevents bootstrap logic from being pointed at files outside the plugin directory.","triggerScenarios":"A hook command or configured scriptPath containing `../` segments or an absolute path outside the plugin root; symlinked script locations that canonicalize outside the root.","commonSituations":"Copying hook configs between machines with different layouts, referencing a globally installed script path, nested plugins so relative paths resolve unexpectedly.","solutions":["Place the referenced script/file inside the plugin root and use a root-relative path","Remove `..` segments or absolute paths from the hook configuration","Update the plugin's rootDir if the legitimate root changed","Check where symlinks resolve; the canonical path must stay under rootDir"],"exampleFix":"// before\nresolveTarget(pluginRoot, '../bin/helper.js')\n// after\nresolveTarget(pluginRoot, 'bin/helper.js')","handlingStrategy":"validation","validationCode":"const path = require('path');\nfunction isInsidePluginRoot(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// guard scriptPath/result before calling resolveTarget","typeGuard":"const isSafePluginPath = (p) => typeof p === 'string' && !path.isAbsolute(p) && !p.split(path.sep).includes('..');","tryCatchPattern":"try {\n  const resolved = resolveTarget(pluginRoot, scriptPath);\n} catch (err) {\n  if (String(err.message).startsWith('Path traversal rejected:')) {\n    console.error(`Hook script outside plugin root: ${scriptPath}`);\n    return null;\n  }\n  throw err;\n}","preventionTips":["Keep all hook scripts inside the plugin directory","Regenerate hook configs when installing on a new machine instead of copying absolute paths","Resolve symlinks in CI and assert they stay under the plugin root"],"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"}