{"record":{"id":"e23a8575fc5b785e","repo":"agalwood/Motrix","slug":"plugin-fs-path-outside-sandbox","errorCode":"plugin.fs.path_outside_sandbox","errorMessage":"plugin.fs.path_outside_sandbox: resolved path outside sandbox root","messagePattern":"plugin\\.fs\\.path_outside_sandbox: resolved path outside sandbox root","errorType":"validation","errorClass":"FsSandboxError","httpStatus":null,"severity":"critical","filePath":"src/core/plugin/capabilities/fs-sandbox.ts","lineNumber":56,"sourceCode":"    if ((e as NodeJS.ErrnoException).code === 'ENOENT') {\n      real = path.normalize(\n        path.join(\n          await realpath(path.dirname(absolute)),\n          path.basename(absolute)\n        )\n      )\n    } else {\n      throw e\n    }\n  }\n  const realRoot = await realpath(root)\n  const rootSep = realRoot.endsWith(path.sep) ? realRoot : realRoot + path.sep\n  const cmp =\n    process.platform === 'darwin' || process.platform === 'win32'\n      ? (s: string) => s.toLowerCase()\n      : (s: string) => s\n  if (cmp(real) !== cmp(realRoot) && !cmp(real).startsWith(cmp(rootSep))) {\n    throw new FsSandboxError(\n      'plugin.fs.path_outside_sandbox',\n      'plugin.fs.path_outside_sandbox: resolved path outside sandbox root'\n    )\n  }\n  return real\n}\n\nexport async function resolveDeepInsideSandbox(\n  root: string,\n  userPath: string\n): Promise<string> {\n  if (userPath.length > PATH_MAX) {\n    throw new FsSandboxError(\n      'plugin.fs.path_too_long',\n      `plugin.fs.path_too_long: path exceeds ${PATH_MAX} characters`\n    )\n  }\n  const normalized = userPath.normalize('NFC')","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/capabilities/fs-sandbox.ts#L38-L74","documentation":"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`.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait storage.read('logs/current')  // 'logs/current' -> /var/log/app via symlink\n\n// after — keep target inside the sandbox\nawait storage.read('logs/app-current')  // real file inside root","handlingStrategy":"try-catch","validationCode":"function hasTraversal(rel: string): boolean {\n  return rel.split(/[\\\\/]/).includes('..')\n}","typeGuard":"function isOutsideSandbox(e: unknown): boolean {\n  return e instanceof Error && (e as FsSandboxError).code === 'plugin.fs.path_outside_sandbox'\n}","tryCatchPattern":"try {\n  await storage.read(rel)\n} catch (e) {\n  if (isOutsideSandbox(e)) {\n    // security-relevant: log, deny, and audit — do not silently continue\n  } else throw e\n}","preventionTips":["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."],"tags":["fs","sandbox","security","symlinks","path-traversal"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}