{"record":{"id":"e468722bc6c9dfa8","repo":"can1357/oh-my-pi","slug":"scheme-path-escapes-its-root-path-e46872","errorCode":null,"errorMessage":"#{scheme}:// path escapes its root: #{path}","messagePattern":"#(.+?):// path escapes its root: #(.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/eval/rb/prelude.rb","lineNumber":52,"sourceCode":"    scheme = m[1].downcase\n    roots =\n      begin\n        raw = ENV[\"PI_EVAL_LOCAL_ROOTS\"]\n        raw && !raw.empty? ? JSON.parse(raw) : {}\n      rescue StandardError\n        {}\n      end\n    root = roots.is_a?(Hash) ? roots[scheme] : nil\n    raise \"Protocol paths are not supported by this helper: #{path}\" if root.nil? || root.to_s.empty?\n    relative = __omp_url_decode(m[2].tr(\"\\\\\", \"/\"))\n    root_path = File.absolute_path(root.to_s)\n    return root_path if relative.empty?\n    if relative.start_with?(\"/\") || relative.split(\"/\").include?(\"..\")\n      raise \"Unsafe #{scheme}:// path (absolute or traversal): #{path}\"\n    end\n    resolved = File.absolute_path(File.join(root_path, relative))\n    unless resolved == root_path || resolved.start_with?(root_path + File::SEPARATOR)\n      raise \"#{scheme}:// path escapes its root: #{path}\"\n    end\n    resolved\n  end\n\n  # -------------------------------------------------------------------------\n  # Display + status\n  # -------------------------------------------------------------------------\n\n  def display(value)\n    __omp_present(value, \"display\")\n    nil\n  end\n\n  # Emit a base64 image as a display output. `mime_type` is \"image/png\" (default)\n  # or \"image/jpeg\"; the host surfaces it as an inspectable image block.\n  def display_image(base64, mime_type: \"image/png\")\n    __omp_emit_display({ mime_type.to_s => base64.to_s })\n    nil","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/eval/rb/prelude.rb#L34-L70","documentation":"After building the absolute candidate `File.absolute_path(File.join(root_path, relative))`, `__omp_resolve_path` verifies the result is still inside the configured scheme root (equal to it or under it with a separator). This is a final containment check that catches symlinks or normalization quirks the earlier lexical `..` check missed; if the resolved path escapes the root, the helper refuses to proceed for `read`/`write`.","triggerScenarios":"A `scheme://` path whose lexical segments are clean but whose resolution lands outside the root — most commonly a symlink inside the root pointing to an outside target (e.g. root contains `link -> /etc` and you call `read(\"local://link/passwd\")`), or a root path with trailing-slash/normalization edge cases such that `File.absolute_path(root)` and the joined result diverge.","commonSituations":"Eval sandboxes where the local root contains symlinks created by setup scripts; roots given with relative paths or `~` expansions that normalize differently on the two sides of the comparison; writing through a symlink placed in the output directory.","solutions":["Remove or repoint the symlink inside the root that resolves outside it, then retry the read/write","Check PI_EVAL_LOCAL_ROOTS for the scheme and confirm the root itself is an absolute, canonical directory (`File.realpath`); fix the configured value if it is relative or unusual","If the target is legitimately outside the root, copy/link it into the root or use an allowed plain filesystem path instead"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  data = read(\"local://link/out.txt\")\nrescue RuntimeError => e\n  raise unless e.message.include?(\"escapes its root\")\n  # inspect the symlink inside the root: File.realpath(\"#{root}/link\")\n  fallback = read_absolute_if_allowed(real_target)\nend","preventionTips":["Audit the scheme root for symlinks pointing outside it (find root -type l)","Configure PI_EVAL_LOCAL_ROOTS with canonical absolute paths (File.realpath output)","Treat any occurrence as evidence the sandbox root is not self-contained"],"tags":["ruby","path-traversal","security","symlink"],"backgroundTag":"path-escapes-root","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}