agalwood/Motrix · critical · FsSandboxError
plugin.fs.path_outside_sandbox
plugin.fs.path_outside_sandbox
Error message
plugin.fs.path_outside_sandbox: resolved path outside sandbox root
What it means
Thrown by `resolveInsideSandbox` after `realpath()` succeeds: the resolved real path is neither identical to the sandbox root nor does it have the root as a prefix (comparison is case-insensitive on darwin/win32, case-sensitive elsewhere). This is the core sandbox-escape guard — symlinks pointing outside the root are detected here. Code is `plugin.fs.path_outside_sandbox`.
Source
Thrown at src/core/plugin/capabilities/fs-sandbox.ts:56
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
real = path.normalize(
path.join(
await realpath(path.dirname(absolute)),
path.basename(absolute)
)
)
} else {
throw e
}
}
const realRoot = await realpath(root)
const rootSep = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep
const cmp =
process.platform === 'darwin' || process.platform === 'win32'
? (s: string) => s.toLowerCase()
: (s: string) => s
if (cmp(real) !== cmp(realRoot) && !cmp(real).startsWith(cmp(rootSep))) {
throw new FsSandboxError(
'plugin.fs.path_outside_sandbox',
'plugin.fs.path_outside_sandbox: resolved path outside sandbox root'
)
}
return real
}
export async function resolveDeepInsideSandbox(
root: string,
userPath: string
): Promise<string> {
if (userPath.length > PATH_MAX) {
throw new FsSandboxError(
'plugin.fs.path_too_long',
`plugin.fs.path_too_long: path exceeds ${PATH_MAX} characters`
)
}
const normalized = userPath.normalize('NFC')View on GitHub (pinned to 1a708ee577)
Solutions
- Audit any symlinks created inside the sandbox and ensure they only target paths inside the same root.
- If the file legitimately lives outside the sandbox, move it under root or configure the sandbox root to include it.
- Reject user-supplied paths containing `..` segments before they reach the fs API.
- Run the operation in a chroot/container where the visible root matches the sandbox root.
Example fix
// before
await storage.read('logs/current') // 'logs/current' -> /var/log/app via symlink
// after — keep target inside the sandbox
await storage.read('logs/app-current') // real file inside root Defensive patterns
Strategy: try-catch
Validate before calling
function hasTraversal(rel: string): boolean {
return rel.split(/[\\/]/).includes('..')
} Type guard
function isOutsideSandbox(e: unknown): boolean {
return e instanceof Error && (e as FsSandboxError).code === 'plugin.fs.path_outside_sandbox'
} Try / catch
try {
await storage.read(rel)
} catch (e) {
if (isOutsideSandbox(e)) {
// security-relevant: log, deny, and audit — do not silently continue
} else throw e
} Prevention
- Reject '..' segments at the input boundary.
- Audit the sandbox for symlinks that escape the root.
- Treat path_outside_sandbox as a security signal, not a transient error.
When it happens
Trigger: A relPath that, possibly via a symlink inside the sandbox, resolves to a location outside `root`. Examples: a symlinked file/dir under the sandbox pointing to `/etc` or `../../`; case-trick paths on case-insensitive filesystems.
Common situations: User content unpacked into the sandbox contains symlinks; a legitimate symlink the plugin created points to shared storage outside the root; cross-platform code where the same path resolves differently on macOS/Windows vs Linux.
Related errors
- plugin.fs.invalid_basename
- plugin.fs.path_too_long
- manifest too large: ${text.length} > ${max}
- plugin.fs.not_found
- plugin.fs.not_a_file
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/e23a8575fc5b785e.
Report an issue: GitHub.