Skip to content

fix: harden fastRemoveDirectory against TOCTOU symlink attacks (#417)#424

Open
kexi wants to merge 2 commits intodevelopfrom
fix/417
Open

fix: harden fastRemoveDirectory against TOCTOU symlink attacks (#417)#424
kexi wants to merge 2 commits intodevelopfrom
fix/417

Conversation

@kexi
Copy link
Copy Markdown
Owner

@kexi kexi commented Apr 28, 2026

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:

  • 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).
  • Fix inverted JSDoc on RuntimeFS.lstat / stat; 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.

Test Plan

  • Added 7 regression tests in fast-remove.test.ts:
    • rejects symlink to directory (sensitive target survives intact)
    • rejects symlink to file
    • rejects regular file
    • rejects broken symlink (not silently treated as not-found)
    • rejects when immediate parent is a symlink
    • succeeds when only a deeper ancestor is a symlink (regression guard for macOS /var → /private/var etc.)
    • cleanupStaleTrash skips .vibe-trash-* symlinks
  • pnpm run check:all passes (lint + typecheck + 416/416 tests + docs Astro check + video typecheck)
  • Independent code review and security audit performed; all CRITICAL/HIGH findings resolved with no new issues introduced.

Checklist

  • Tests added/updated
  • pnpm run check:all passes
  • Docs updated (if needed) — N/A (no command/option changes; only internal security hardening)

Fixes #417

kexi and others added 2 commits April 28, 2026 22:42
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>
@kexi kexi self-assigned this Apr 28, 2026
@kexi kexi marked this pull request as ready for review April 28, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security][High] Symlink attack in fast-remove directory deletion

1 participant