Conversation
Defend against Time-Of-Check / Time-Of-Use symlink-swap attacks in the
worktree-removal path. Previously, an attacker with write access to a
worktree's parent directory could replace the target between the
existence check and the rename/trash invocation, redirecting the
operation to an attacker-chosen location.
Defense-in-depth changes in fastRemoveDirectory:
- Replace exists() with lstat()-based validateRemovalTarget; refuse
symlinks and non-directories outright.
- Re-validate the leaf and immediate parent (lstat, not realPath
string compare) immediately before each external sink: native trash
adapter, AppleScript fallback, temp-dir rename, parent-dir rename.
- After each rename, re-lstat the destination; on TOCTOU detection,
roll back and skip background deletion entirely.
- Harden moveToMacOSTrashViaAppleScript with leaf+parent lstat checks
(Finder may follow leaf symlinks).
- Extend CONTROL_CHARS_PATTERN to cover U+2028 / U+2029 (AppleScript
line terminators).
- Filter symlinks from cleanupStaleTrash readDir enumeration and
re-lstat each candidate before spawning rm -rf.
- Bump trash-name UUID suffix from 8 to 16 hex chars (64 bits).
Also fix RuntimeFS.lstat / stat JSDoc which previously had inverted
descriptions of symlink-following behavior; the security claim depends
on lstat NOT following symlinks.
Behavior change for callers: fastRemoveDirectory now returns
{success: false} for symlink / non-directory targets. The single
production caller (removeWorktree in clean.ts) already falls back to
git worktree remove on success: false, preserving graceful behavior
for any legitimate path.
Closes #417
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Runtime state file from the Claude Code scheduling harness; not project content. Co-Authored-By: Claude Opus 4.7 <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.
Summary
Fixes #417 (Severity: High). Defends against Time-Of-Check / Time-Of-Use symlink-swap attacks in
fastRemoveDirectory. Previously, an attacker with write access to a worktree's parent directory could replace the target between the existence check and the rename/trash invocation, redirecting the operation to an attacker-chosen location.Defense-in-depth changes:
exists()withlstat()-basedvalidateRemovalTarget; refuse symlinks and non-directories outright.lstat, notrealPathstring compare) immediately before each external sink: native trash adapter, AppleScript fallback, temp-dir rename, parent-dir rename.lstatthe destination; on TOCTOU detection, roll back and skip background deletion entirely.moveToMacOSTrashViaAppleScriptwith leaf+parentlstatchecks (Finder may follow leaf symlinks).CONTROL_CHARS_PATTERNto cover U+2028 / U+2029 (AppleScript line terminators).cleanupStaleTrashreadDirenumeration and re-lstateach candidate before spawningrm -rf.RuntimeFS.lstat/stat; the security claim depends onlstatNOT following symlinks.Behavior change for callers:
fastRemoveDirectorynow returns{success: false}for symlink / non-directory targets. The single production caller (removeWorktreeinclean.ts) already falls back togit worktree removeonsuccess: false, preserving graceful behavior for any legitimate path.Test Plan
fast-remove.test.ts:/var → /private/varetc.)cleanupStaleTrashskips.vibe-trash-*symlinkspnpm run check:allpasses (lint + typecheck + 416/416 tests + docs Astro check + video typecheck)Checklist
pnpm run check:allpassesFixes #417