{"record":{"id":"d81c23cb0c99ee98","repo":"CherryHQ/cherry-studio","slug":"agent-storage-path-resolves-outside-its-root-ta","errorCode":null,"errorMessage":"Agent storage path resolves outside its root: ${target}","messagePattern":"Agent storage path resolves outside its root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"src/main/ai/agents/agentDataDirectory.ts","lineNumber":76,"sourceCode":"  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}`)\n    }\n  }\n\n  const [realRoot, realTarget] = await Promise.all([\n    resolveRealOrNearestExistingPath(root),\n    resolveRealOrNearestExistingPath(target)\n  ])\n  if (realTarget !== realRoot && !isPathInside(realTarget, realRoot)) {\n    throw new Error(`Agent storage path resolves outside its root: ${target}`)\n  }\n}\n\n/** Ensure a Data/Agents path is a real directory contained by the Agent storage root. */\nexport async function ensureAgentStorageDirectory(agentsDataRoot: string, targetPath: string): Promise<void> {\n  await ensureDir(asAbsolutePath(path.resolve(agentsDataRoot)))\n  await assertAgentStoragePath(agentsDataRoot, targetPath)\n  await ensureDir(asAbsolutePath(path.resolve(targetPath)))\n  await assertAgentStoragePath(agentsDataRoot, targetPath)\n  const targetStat = await lstat(asAbsolutePath(path.resolve(targetPath)))\n  if (!targetStat.isDirectory || targetStat.isSymbolicLink) {\n    throw new Error(`Agent storage directory must be a real directory: ${targetPath}`)\n  }\n}\n\nfunction assertAgentId(agentId: string): void {\n  if (!agentId || agentId === '.' || agentId === '..' || agentId.toLowerCase() === 'system' || /[\\\\/]/.test(agentId)) {\n    throw new Error(`Invalid agent id for data directory: ${agentId}`)","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/agents/agentDataDirectory.ts#L58-L94","documentation":"Thrown by assertAgentStoragePath as the final defense: after the lexical and lstat checks pass, the code resolves the real (symlink-followed) paths of both root and target via realpath and confirms the real target is still inside the real root. This catches a symlink OUTSIDE the managed tree (e.g. a parent directory above the root being a symlink) that the in-tree walk would not see, closing the gap between lexical containment and actual filesystem resolution.","triggerScenarios":"A symlink exists above or alongside the managed root such that, once resolved, the target's real path lies outside the real root — e.g. the root itself is reachable through a link, or an external symlink was injected that the segment walk (limited to root..target) did not traverse.","commonSituations":"The agentsDataRoot path itself contains a symlink component (e.g. /Users/X is a link); a symlink in a parent directory redirects resolution outside; a creative traversal that passes lexical checks but escapes on realpath; containerized/mapped volumes where paths differ from the host.","solutions":["Resolve the agentsDataRoot to its real path at configuration time and store/use the realpath consistently.","Remove symlinks in the parent chain of the data root, or point agentsDataRoot at a real path with no symlinked ancestors.","Ensure the data root lives on a real directory tree not under any symlinked parent (common with cloud-synced home folders).","If on a mapped/container volume, verify the host path the container sees matches expectations."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { realpath } from 'node:fs/promises'\nimport path from 'node:path'\nimport { isPathInside } from '@main/utils/file'\n\nconst realRoot = await realpath(path.resolve(agentsDataRoot))\nconst realTarget = await realpath(path.resolve(targetPath))\nif (realTarget !== realRoot && !isPathInside(realTarget, realRoot)) {\n  throw new Error(`Refusing: real path '${realTarget}' escapes real root '${realRoot}'`)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Store and use the realpath of the agents root at configuration time.","Avoid placing the data root under symlinked parent directories (common with cloud-synced homes).","On container/volume setups, verify the host path matches what the container resolves."],"tags":["security","filesystem","symlink","path-traversal","agent"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}