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

  1. Ensure targetPath is always constructed as a descendant of agentsDataRoot (use path.join(agentsDataRoot, agentId, ...) rather than concatenating raw input).
  2. Validate the agentId via agentDataDirectoryPath/assertAgentId first to reject path separators and special names.
  3. Pass absolute, normalized paths to assertAgentStoragePath; avoid mixing relative and absolute roots.
  4. 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

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


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/277677c2f13d74f0. Report an issue: GitHub.