Skip to content

fix(sandbox): keep relative in-repo symlinks on download - #1177

Merged
ralphbean merged 4 commits into
mainfrom
fix/sanitize-download-keep-inrepo-symlinks
May 19, 2026
Merged

fix(sandbox): keep relative in-repo symlinks on download#1177
ralphbean merged 4 commits into
mainfrom
fix/sanitize-download-keep-inrepo-symlinks

Conversation

@rh-hemartin

@rh-hemartin rh-hemartin commented May 19, 2026

Copy link
Copy Markdown
Member

Summary

  • sanitizeDownload previously removed all symlinks unconditionally, stripping tracked relative symlinks (.claude/skills, etc.) from the working tree after download
  • Post-run make lint then failed in lint-broken-symlinks because git-tracked paths were absent
  • Now keeps symlinks whose relative target resolves inside the repo root; removes absolute targets and relative targets that escape via ../

Test plan

  • TestSanitizeDownload_RemovesAbsoluteSymlinks — absolute symlinks still removed
  • TestSanitizeDownload_KeepsRelativeSymlinksInsideRepo — relative in-repo symlinks preserved
  • TestSanitizeDownload_RemovesRelativeSymlinksEscapingRepo — traversal-escape symlinks removed
  • All existing TestSanitizeDownload_* tests pass

Fixes #1149.

🤖 Generated with Claude Code

sanitizeDownload removed all symlinks unconditionally. This caused
lint-broken-symlinks to fail post-run because tracked relative symlinks
(e.g. .claude/skills) were stripped from the working tree.

Keep symlinks whose relative target resolves inside the repo root.
Remove absolute targets and relative targets that escape via ../.

Fixes #1149.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown

Site preview

Preview: https://02e7487f-site.fullsend-ai.workers.dev

Commit: 481c27e2a5c898855546aba5cbcbaa5f59b3917c

@fullsend-ai-review

fullsend-ai-review Bot commented May 19, 2026

Copy link
Copy Markdown

Review

Findings

No findings.

Previous run

Review

Findings

No findings.

Previous run (2)

Review

Findings

No findings.

Previous run (3)

Review

Findings

Low

  • [documentation-currency] internal/sandbox/sandbox.go:346 — The SafeDownload doc comment says "sanitizes the result by removing symlinks and .git/hooks/" — this now only removes dangerous symlinks (absolute or repo-escaping). The same stale phrasing appears in internal/cli/run.go:557, docs/guides/dev/cli-internals.md:319, and docs/guides/dev/cli-internals.md:326-327. All four locations should be updated to say something like "removes dangerous symlinks (absolute or repo-escaping) and .git/hooks/".
    Remediation: Update the doc comment on SafeDownload, the inline comment in run.go, and the two references in cli-internals.md to reflect the new selective-removal behavior.

SafeDownload now keeps relative in-repo symlinks; update all four
doc sites (sandbox.go, run.go, cli-internals.md ×2) to say
"dangerous symlinks (absolute or repo-escaping)" instead of "symlinks".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label May 19, 2026
@rh-hemartin

Copy link
Copy Markdown
Member Author

Tested on a run as well with the vendored binary.

@rh-hemartin rh-hemartin self-assigned this May 19, 2026

@ralphbean ralphbean left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See inline comment.

Comment thread internal/sandbox/sandbox.go Outdated
filepath.Clean is textual: "sub/link/../../etc/passwd" resolves to
inside the repo, but if link -> ../real the filesystem follows it to
/etc/passwd. EvalSymlinks follows intermediate symlink components,
so the resolved path correctly lands outside the repo and is removed.

Fall back to remove on any EvalSymlinks error (dangling/looping link).

Adds TestSanitizeDownload_RemovesSymlinkChainEscape to verify the fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed ready-for-merge All reviewers approved — ready to merge labels May 19, 2026
@rh-hemartin

Copy link
Copy Markdown
Member Author

Fixed in 1f8cb15. Replaced filepath.Clean(filepath.Join(...)) with filepath.EvalSymlinks on the raw (un-cleaned) path so intermediate symlink components are followed on the filesystem rather than collapsed textually. Added TestSanitizeDownload_RemovesSymlinkChainEscape which creates exactly the chain you described (sub/dirlink -> ../real, escape -> sub/dirlink/../../etc/passwd) and verifies escape is removed.

Two improvements over the previous commit:

1. Use EvalSymlinks(absLocal) instead of filepath.Clean so the root path
   is fully resolved before comparison. On systems where os.TempDir()
   returns a symlinked path (e.g. macOS /tmp -> /private/tmp), Clean
   leaves the symlink component, causing HasPrefix to incorrectly reject
   safe symlinks whose resolved path starts with the real prefix.

2. Add TestSanitizeDownload_RemovesDirSymlinkIndirection (from ralphbean's
   branch) covering the "d/x -> ." attack: a dot-symlink kept as in-repo
   adds an extra level so "d/x/../../secret" escapes textually but is
   caught by EvalSymlinks on the raw path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rh-hemartin

Copy link
Copy Markdown
Member Author

Updated in 481c27e. Two additions from ralphbean's branch:

  1. EvalSymlinks(absLocal) instead of filepath.Clean — on macOS where /tmp -> /private/tmp, Clean leaves the symlink component in absLocal, causing the HasPrefix comparison to incorrectly reject safe symlinks whose EvalSymlinks result starts with the real prefix (/private/tmp/...).

  2. Added TestSanitizeDownload_RemovesDirSymlinkIndirection (from tmp/sanitize-download-keep-inrepo-symlinks). The d/x -> . attack wasn't covered by my earlier test — wanted both cases in the suite.

The core approach stays different from the patch on your branch: I call EvalSymlinks on the raw path (dir + "/" + target without filepath.Clean) rather than on the cleaned path. This matters when repo/secret exists as a real file inside the repo — with the cleaned path, EvalSymlinks would return repo/secret and call it safe, even though following the symlink chain on the filesystem reaches the outer secret file. The raw path approach follows the chain as the OS would and returns the physical outside path.

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge and removed ready-for-merge All reviewers approved — ready to merge labels May 19, 2026
@ralphbean
ralphbean added this pull request to the merge queue May 19, 2026
Merged via the queue into main with commit 9a05a7f May 19, 2026
11 of 12 checks passed
@ralphbean
ralphbean deleted the fix/sanitize-download-keep-inrepo-symlinks branch May 19, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-merge All reviewers approved — ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint-broken-symlinks and SafeDownload conflict: one enforces valid symlinks, the other removes them

2 participants