{"record":{"id":"e9ea5fbac75cbbc9","repo":"can1357/oh-my-pi","slug":"scheme-path-escapes-its-root-rawpath","errorCode":null,"errorMessage":"${scheme}:// path escapes its root: ${rawPath}","messagePattern":"(.+?):// path escapes its root: (.+?)","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/eval/js/shared/helpers.ts","lineNumber":141,"sourceCode":"function resolveUnderRoot(scheme: string, root: string, rawRelative: string, rawPath: string): string {\n\tlet relative: string;\n\ttry {\n\t\trelative = decodeURIComponent(rawRelative.replaceAll(\"\\\\\", \"/\"));\n\t} catch {\n\t\tthrow new ToolError(`Invalid URL encoding in ${scheme}:// path: ${rawPath}`);\n\t}\n\tconst rootPath = path.resolve(root);\n\tif (relative === \"\") return rootPath;\n\tif (path.isAbsolute(relative)) {\n\t\tthrow new ToolError(`Absolute paths are not allowed in ${scheme}:// URLs: ${rawPath}`);\n\t}\n\tconst normalized = path.normalize(relative);\n\tif (normalized.startsWith(\"..\") || normalized.includes(\"/../\") || normalized.includes(\"/..\")) {\n\t\tthrow new ToolError(`Path traversal (..) is not allowed in ${scheme}:// URLs: ${rawPath}`);\n\t}\n\tconst resolved = path.resolve(rootPath, normalized);\n\tif (resolved !== rootPath && !resolved.startsWith(`${rootPath}${path.sep}`)) {\n\t\tthrow new ToolError(`${scheme}:// path escapes its root: ${rawPath}`);\n\t}\n\treturn resolved;\n}\n\nasync function resolveRegularFile(\n\tctx: HelperContext,\n\trawPath: string,\n): Promise<{ filePath: string; file: Bun.BunFile; size: number }> {\n\tconst filePath = resolveHelperPath(ctx, rawPath, \"read\");\n\tconst file = Bun.file(filePath);\n\tconst stat = await file.stat();\n\tif (stat.isDirectory()) {\n\t\tthrow new ToolError(`Directory paths are not supported by read(): ${filePath}`);\n\t}\n\treturn { filePath, file, size: stat.size };\n}\n\nfunction getDataSize(data: string | Blob | ArrayBuffer | ArrayBufferView): number {","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/eval/js/shared/helpers.ts#L123-L159","documentation":"As a final confinement check, `resolveUnderRoot` resolves the normalized path against the root and verifies the result still lies inside it (`resolved === rootPath` or starts with `rootPath + sep`). If the resolved path lands outside — e.g. via symlinks or root-edge tricks that earlier textual checks miss — this ToolError is thrown.","triggerScenarios":"A `scheme://` relative path whose resolution escapes the mounted root despite passing the textual `..` checks — for instance `local://..` exactly at the root edge, or a relative path combined with a symlinked root resolved to a different prefix.","commonSituations":"Edge-case paths like `local://.` or `local://..` that survive string checks; roots mounted through symlinks so `path.resolve` produces a prefix differing from the configured root string.","solutions":["Use a plain file path relative to the root without edge segments (`.` or `..`).","Ensure the localRoot passed by the host is a fully resolved, symlink-free directory (host-side fix: `path.realpathSync(root)`).","Read the root directory itself via a dedicated listing helper if available rather than resolving the root edge path."],"exampleFix":"// before\nawait read(\"local://..\"); // resolves outside root\n// after\nawait read(\"local://sub/dir/file.md\");","handlingStrategy":"validation","validationCode":"const root = path.resolve(mountedRoot);\nconst resolved = path.resolve(root, rel);\nif (resolved !== root && !resolved.startsWith(root + path.sep)) throw new Error(\"escapes root\");","typeGuard":null,"tryCatchPattern":"try {\n\treturn await read(url);\n} catch (err) {\n\tif (String(err?.message).includes(\"escapes its root\")) {\n\t\t// root likely symlinked; ask host to re-mount with realpath or use plain path\n\t}\n\tthrow err;\n}","preventionTips":["Mount localRoots as realpath-resolved directories to avoid prefix mismatches.","Avoid edge segments (`.`/`..`) in scheme URLs.","Keep targets as direct descendants of the mounted root."],"tags":["security","sandbox","path-traversal","symlink"],"backgroundTag":"sandbox-root-escape","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}