diff options
| author | Adam Malczewski <[email protected]> | 2026-03-31 23:10:45 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-03-31 23:10:45 +0900 |
| commit | 57c56daf5906442dacc15951c9b3405f89309839 (patch) | |
| tree | e76890119c0e47acb48f8585222b7a2f5e22df56 /lib/dispatch/tool/files/sandbox.rb | |
| parent | 25488d32336e05b69a41391cc7b5153478d3cc8a (diff) | |
| download | dispatch-tool-files-dev.tar.gz dispatch-tool-files-dev.zip | |
impdev
Diffstat (limited to 'lib/dispatch/tool/files/sandbox.rb')
| -rw-r--r-- | lib/dispatch/tool/files/sandbox.rb | 69 |
1 files changed, 69 insertions, 0 deletions
diff --git a/lib/dispatch/tool/files/sandbox.rb b/lib/dispatch/tool/files/sandbox.rb new file mode 100644 index 0000000..6ceccb1 --- /dev/null +++ b/lib/dispatch/tool/files/sandbox.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Dispatch + module Tool + module Files + module Sandbox + module_function + + def resolve_path(path, worktree_path:) + worktree_real = File.realpath(worktree_path) + + # Join the path against the worktree root + joined = File.expand_path(path, worktree_real) + + # Check the expanded (logical) path first — catches ../traversal and absolute paths + unless joined.start_with?("#{worktree_real}/") || joined == worktree_real + raise SandboxError, "Path '#{path}' resolves outside the worktree sandbox" + end + + # For existing paths, resolve symlinks and verify the real path + if File.exist?(joined) + real = File.realpath(joined) + + unless real.start_with?("#{worktree_real}/") || real == worktree_real + raise SandboxError, "Path '#{path}' resolves outside the worktree sandbox (symlink escape)" + end + + real + else + # For non-existent paths, resolve as much of the existing prefix as possible + # to catch symlinked directory components pointing outside the worktree + parts = joined.delete_prefix("#{worktree_real}/").split("/") + current = worktree_real + + parts.each_with_index do |part, i| + candidate = File.join(current, part) + + if File.symlink?(candidate) + real_target = File.realpath(candidate) + + unless real_target.start_with?("#{worktree_real}/") || real_target == worktree_real + raise SandboxError, "Path '#{path}' resolves outside the worktree sandbox (symlink escape)" + end + + current = real_target + elsif File.exist?(candidate) + current = File.realpath(candidate) + else + # Remainder of the path doesn't exist on disk — that's fine, return the logical path + remaining = parts[(i + 1)..] + + return remaining.empty? ? candidate : File.join(candidate, *remaining) + end + end + + current + end + end + + def within_worktree?(path, worktree_path:) + resolve_path(path, worktree_path:) + true + rescue SandboxError + false + end + end + end + end +end |
