fix(security): refuse symlink at .worktrees before creating a worktree (closes #14)#18
Merged
Merged
Conversation
Closes #14 (CRITICAL). createWorktree did fs.mkdirSync(<repoRoot>/.worktrees, {recursive:true}) — Node's recursive mkdir follows a symlink at the final component. A crafted repo, a co-resident user, or a prior malicious run that did `ln -s ~/.config .worktrees` would redirect the subsequent `git worktree add` (the Codex workspace-write root) into ~/.ssh / ~/.config / ~/Library/LaunchAgents — a sandbox escape + persistence/credential primitive. Attack reproduced end-to-end before the fix: `.worktrees -> /tmp/attacker-dest` caused `git worktree add` to populate /tmp/attacker-dest/codex-<ts>/ with HEAD content. Fix: before mkdirSync, lstat .worktrees and throw if it is a symlink; after creation, verify the resolved realpath is contained within repoRoot. The leave-branch invariant is preserved (no new Destroy path — this guards creation). Regression test: plant a hostile symlink `.worktrees -> <tmp outside repoRoot>`, assert createWorktreeSession throws and the attacker destination stays empty. Verified: worktree 13/13; full npm test 156 pass / 4 pre-existing (unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0189cGaohsYdpkB4otKZnpAt
Owner
Author
🔍 Local review (cycle 1) — security
Triage: 0 FIX in scope. #14 closed as scoped. Local mode — merge on user trigger. |
axisrow
added a commit
that referenced
this pull request
Jul 20, 2026
Security hardening release: leave-branch worktree cleanup (#12) + 4 security fixes (#18 symlink guard, #20 core.symlinks=false neutralize, #22 eliminate info/exclude write, #23 accumulation warn). rescue default --background (#8). test-teardown (#5). All verified with cycle-review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #14 (CRITICAL, security review of #12).
Vulnerability
createWorktreedidfs.mkdirSync(<repoRoot>/.worktrees, {recursive:true}). Node's recursive mkdir follows a symlink at the final component. A crafted repo, a co-resident user, or a prior malicious run that didln -s ~/.config .worktreesredirects the subsequentgit worktree add— the Codexworkspace-writeroot — into~/.ssh,~/.config,~/Library/LaunchAgents, etc. Sandbox escape + persistence/credential-injection primitive.Attack reproduced end-to-end before the fix:
.worktrees -> /tmp/attacker-destcausedgit worktree addto populate/tmp/attacker-dest/codex-<ts>/with HEAD content.Fix
Before
mkdirSync,lstat.worktreesand throw if it is a symlink; after creation, verify the resolved realpath is contained within repoRoot. The leave-branch invariant is preserved (no new Destroy path — this guards creation only).Verified
.worktrees -> <tmp outside repoRoot>, assertcreateWorktreeSessionthrows and the attacker destination stays empty.git worktree addpopulates the target).worktree.test.mjs13/13; fullnpm test156 pass / 4 pre-existing (unchanged).Severity note (from #14): code-path-severity-once-wired —
createWorktreeSessionhas no production callers today (#12 was the library layer); this closes the hole before the companion--worktreewiring lands.