{"record":{"id":"277677c2f13d74f0","repo":"CherryHQ/cherry-studio","slug":"agent-storage-path-escapes-its-root-target","errorCode":null,"errorMessage":"Agent storage path escapes its root: ${target}","messagePattern":"Agent storage path escapes its root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"src/main/ai/agents/agentDataDirectory.ts","lineNumber":49,"sourceCode":"        return asAbsolutePath(path.normalize(path.join(realCurrentPath, path.relative(currentPath, targetPath))))\n      } catch {\n        const parentPath = asAbsolutePath(path.dirname(currentPath))\n        if (parentPath === currentPath) return asAbsolutePath(path.normalize(targetPath))\n        currentPath = parentPath\n      }\n    }\n  }\n}\n\n/**\n * Validate a path in Data/Agents without following symbolic links in the\n * managed root or any path component below it.\n */\nexport async function assertAgentStoragePath(agentsDataRoot: string, targetPath: string): Promise<void> {\n  const root = asAbsolutePath(path.resolve(agentsDataRoot))\n  const target = asAbsolutePath(path.resolve(targetPath))\n  if (target !== root && !isPathInside(target, root)) {\n    throw new Error(`Agent storage path escapes its root: ${target}`)\n  }\n\n  const rootStat = await lstatIfExists(root)\n  if (!rootStat?.isDirectory || rootStat.isSymbolicLink) {\n    throw new Error(`Agent storage root must be a real directory: ${root}`)\n  }\n\n  let current = root\n  const relative = path.relative(root, target)\n  for (const segment of relative ? relative.split(path.sep) : []) {\n    current = asAbsolutePath(path.join(current, segment))\n    const currentStat = await lstatIfExists(current)\n    if (!currentStat) break\n    if (currentStat.isSymbolicLink) {\n      throw new Error(`Agent storage path contains a symbolic link: ${current}`)\n    }\n    if (current !== target && !currentStat.isDirectory) {\n      throw new Error(`Agent storage path parent is not a directory: ${current}`)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/agents/agentDataDirectory.ts#L31-L67","documentation":"Thrown by assertAgentStoragePath when the resolved target path is not equal to and not lexically inside the agents data root. This is the first line of defense in a layered path-traversal guard: it catches a targetPath that resolves outside Data/Agents (e.g. ../../etc/passwd) using string comparison BEFORE touching the filesystem, so it blocks traversal even when components do not yet exist.","triggerScenarios":"Passing a targetPath containing '..' segments that resolve above the agents root; an agentId-derived path where the agentId contained path separators (normally blocked earlier by assertAgentId, but a direct caller could bypass); a misconfigured agentsDataRoot that differs from where the path was constructed.","commonSituations":"A caller constructs a path from unvalidated user input; a bug joins the wrong root with a target; symlink-unaware path math; a migrated data root while old absolute paths linger.","solutions":["Ensure targetPath is always constructed as a descendant of agentsDataRoot (use path.join(agentsDataRoot, agentId, ...) rather than concatenating raw input).","Validate the agentId via agentDataDirectoryPath/assertAgentId first to reject path separators and special names.","Pass absolute, normalized paths to assertAgentStoragePath; avoid mixing relative and absolute roots.","If the root moved, re-derive all stored paths from the new root instead of reusing old absolute strings."],"exampleFix":"// before\nconst target = path.join(agentsDataRoot, userInput) // userInput may contain '..'\nawait assertAgentStoragePath(agentsDataRoot, target)\n\n// after\nconst agentId = assertAgentId(userInput) // rejects separators / traversal\nconst target = path.join(agentsDataRoot, agentId)\nawait assertAgentStoragePath(agentsDataRoot, target)","handlingStrategy":"validation","validationCode":"import path from 'node:path'\nimport { isPathInside } from '@main/utils/file'\n\nconst root = path.resolve(agentsDataRoot)\nconst target = path.resolve(targetPath)\nif (target !== root && !isPathInside(target, root)) {\n  throw new Error(`Refusing: target '${target}' escapes agents root '${root}'`)\n}","typeGuard":"const isInsideRoot = (target: string, root: string): boolean => {\n  const t = path.resolve(target), r = path.resolve(root)\n  return t === r || isPathInside(t, r)\n}","tryCatchPattern":null,"preventionTips":["Always build target paths with path.join(agentsDataRoot, agentId, ...) — never concatenate raw input.","Validate agentId with assertAgentId before using it in any path.","Prefer the public ensureAgentStorageDirectory over calling assertAgentStoragePath directly."],"tags":["security","path-traversal","filesystem","agent","validation"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}