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

  1. Audit any symlinks created inside the sandbox and ensure they only target paths inside the same root.
  2. If the file legitimately lives outside the sandbox, move it under root or configure the sandbox root to include it.
  3. Reject user-supplied paths containing `..` segments before they reach the fs API.
  4. 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

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


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/e23a8575fc5b785e. Report an issue: GitHub.