{"record":{"id":"4a9da8aeb61439ea","repo":"Egonex-AI/Understand-Anything","slug":"invalid-input-analysispaths-entry-escapes-project","errorCode":null,"errorMessage":"Invalid input: analysisPaths entry escapes projectRoot: ${rawPath}","messagePattern":"Invalid input: analysisPaths entry escapes projectRoot: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"understand-anything-plugin/skills/understand/extract-import-map.mjs","lineNumber":139,"sourceCode":"    }\n    filesByPath.set(toPosix(file.path), file);\n  }\n\n  const selected = [];\n  const seen = new Set();\n  for (const rawPath of analysisPaths) {\n    if (typeof rawPath !== 'string' || rawPath.length === 0) {\n      throw new Error('Invalid input: every analysisPaths entry must be a non-empty string');\n    }\n    // Use the host's path semantics here. On POSIX, backslashes and drive-like\n    // prefixes are ordinary project-relative filename characters; on Windows,\n    // path.isAbsolute also rejects drive-rooted and root-relative paths.\n    if (isAbsolute(rawPath)) {\n      throw new Error(`Invalid input: analysisPaths entry must be project-relative: ${rawPath}`);\n    }\n    const path = toPosix(rawPath);\n    if (!path || path.split('/').some(part => part === '..')) {\n      throw new Error(`Invalid input: analysisPaths entry escapes projectRoot: ${rawPath}`);\n    }\n    const file = filesByPath.get(path);\n    if (!file) {\n      throw new Error(`Invalid input: analysisPaths entry is not present in files: ${rawPath}`);\n    }\n    if (!seen.has(path)) {\n      seen.add(path);\n      selected.push(file);\n    }\n  }\n  return selected;\n}\n\n// ECMAScript relational string comparison is lexicographic over UTF-16 code\n// units, so path ordering is stable across ICU versions, locales, and hosts.\nfunction comparePaths(a, b) {\n  if (a === b) return 0;\n  return a < b ? -1 : 1;","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/Egonex-AI/Understand-Anything/blob/07edf82a04371b6f69779b067bdc8a1a8753a9db/understand-anything-plugin/skills/understand/extract-import-map.mjs#L121-L157","documentation":"After confirming an analysisPaths entry is relative, selectAnalysisFiles() normalizes it to POSIX separators and rejects it if it is empty after normalization or contains any '..' segment. This blocks path entries that would climb out of projectRoot (e.g. \"../secrets.json\" or \"src/../../etc/passwd\"), keeping the extractor from reading files outside the analyzed project.","triggerScenarios":"Passing an analysisPaths entry like \"../other-package/src/index.ts\", \"a/../b/../../x.ts\", or a path that normalizes to empty (\"./\", \".\"). Also triggered on POSIX by entries mixing separators in ways that produce '..' segments after split.","commonSituations":"An incremental caller computes changed files relative to the wrong root (repo root vs project root), producing '..' segments; a template or config interpolates a path like \"${root}/${rel}\" where rel already contains \"..\"; hand-written input JSON with \"./src\" style entries that normalize to empty.","solutions":["Rewrite each entry so it stays inside projectRoot with no '..' segments; resolve against the correct root first, then take path.relative(projectRoot, resolved) and verify it does not start with '..'.","Use paths copied verbatim from the `files` inventory array instead of computing them elsewhere.","Reject or skip out-of-root files in the calling tool before generating the input JSON."],"exampleFix":"// before\nanalysisPaths: [\"../shared/lib.ts\"]\n// after\nconst rel = path.relative(projectRoot, path.resolve(projectRoot, userPath));\nif (rel.startsWith('..')) throw new Error(`${userPath} is outside projectRoot`);\nanalysisPaths: [rel.split(path.sep).join('/')]","handlingStrategy":"validation","validationCode":"function staysInRoot(p, projectRoot) {\n  const rel = relative(projectRoot, resolve(projectRoot, p));\n  return rel !== '' && !rel.startsWith('..') && !resolve(projectRoot, p).startsWith(projectRoot + '..');\n}\nconst safePaths = analysisPaths.filter(p => staysInRoot(p, projectRoot));","typeGuard":"const isInsideProject = (p, root) => { const rel = relative(root, resolve(root, p)); return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); };","tryCatchPattern":"try {\n  await runExtractor(input);\n} catch (err) {\n  if (String(err.message).includes('analysisPaths entry escapes projectRoot')) {\n    input.analysisPaths = input.analysisPaths.filter(p => isInsideProject(p, projectRoot));\n    await runExtractor(input);\n  } else throw err;\n}","preventionTips":["Resolve user-supplied paths against projectRoot and recompute relative(projectRoot, resolved) before use.","Drop '..'-containing entries at the tool boundary; log and skip instead of forwarding them.","Never concatenate root + user path; use path.resolve/path.relative."],"tags":["path-traversal","security","invalid-argument"],"backgroundTag":"path-traversal-blocked","analyzedSha":"07edf82a04371b6f69779b067bdc8a1a8753a9db","analyzedAt":"2026-09-07T23:20:10.829Z","contentChangedAt":"2026-09-07T23:20:10.829Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}