CherryHQ/cherry-studio · critical · Error
Agent storage path escapes its root: ${target}
Error message
Agent storage path escapes its root: ${target} What it means
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.
Source
Thrown at src/main/ai/agents/agentDataDirectory.ts:49
return asAbsolutePath(path.normalize(path.join(realCurrentPath, path.relative(currentPath, targetPath))))
} catch {
const parentPath = asAbsolutePath(path.dirname(currentPath))
if (parentPath === currentPath) return asAbsolutePath(path.normalize(targetPath))
currentPath = parentPath
}
}
}
}
/**
* Validate a path in Data/Agents without following symbolic links in the
* managed root or any path component below it.
*/
export async function assertAgentStoragePath(agentsDataRoot: string, targetPath: string): Promise<void> {
const root = asAbsolutePath(path.resolve(agentsDataRoot))
const target = asAbsolutePath(path.resolve(targetPath))
if (target !== root && !isPathInside(target, root)) {
throw new Error(`Agent storage path escapes its root: ${target}`)
}
const rootStat = await lstatIfExists(root)
if (!rootStat?.isDirectory || rootStat.isSymbolicLink) {
throw new Error(`Agent storage root must be a real directory: ${root}`)
}
let current = root
const relative = path.relative(root, target)
for (const segment of relative ? relative.split(path.sep) : []) {
current = asAbsolutePath(path.join(current, segment))
const currentStat = await lstatIfExists(current)
if (!currentStat) break
if (currentStat.isSymbolicLink) {
throw new Error(`Agent storage path contains a symbolic link: ${current}`)
}
if (current !== target && !currentStat.isDirectory) {
throw new Error(`Agent storage path parent is not a directory: ${current}`)View on GitHub (pinned to 726446b54c)
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.
Example fix
// before const target = path.join(agentsDataRoot, userInput) // userInput may contain '..' await assertAgentStoragePath(agentsDataRoot, target) // after const agentId = assertAgentId(userInput) // rejects separators / traversal const target = path.join(agentsDataRoot, agentId) await assertAgentStoragePath(agentsDataRoot, target)
Defensive patterns
Strategy: validation
Validate before calling
import path from 'node:path'
import { isPathInside } from '@main/utils/file'
const root = path.resolve(agentsDataRoot)
const target = path.resolve(targetPath)
if (target !== root && !isPathInside(target, root)) {
throw new Error(`Refusing: target '${target}' escapes agents root '${root}'`)
} Type guard
const isInsideRoot = (target: string, root: string): boolean => {
const t = path.resolve(target), r = path.resolve(root)
return t === r || isPathInside(t, r)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid agent id for data directory: ${agentId}
- Agent storage root must be a real directory: ${root}
- Agent storage path contains a symbolic link: ${current}
- Agent storage path parent is not a directory: ${current}
- Agent storage path resolves outside its root: ${target}
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/277677c2f13d74f0.
Report an issue: GitHub.