Skip to content

Reject symlink aliases for cancel source and destination#94

Merged
SihaoLiu merged 2 commits intodevfrom
use-realpath4everything
Apr 17, 2026
Merged

Reject symlink aliases for cancel source and destination#94
SihaoLiu merged 2 commits intodevfrom
use-realpath4everything

Conversation

@SihaoLiu
Copy link
Copy Markdown
Contributor

Summary

Follow-up to the canonicalize-paths work already merged into dev via #93. Codex review on that PR flagged a P1 path-restriction bypass: using full-path realpath on the user-provided mv source and destination in is_cancel_authorized lets a symlink aliasing <loop>/state.md or <loop>/cancel-state.md pass authorization. For the destination case this is exploitable — mv <loop>/state.md /tmp/link where /tmp/link -> <loop>/cancel-state.md canonicalizes to the expected path and passes the check, but mv replaces the link at /tmp/link rather than creating <loop>/cancel-state.md, corrupting loop state and depositing state.md outside the loop dir.

  • Introduce canonicalize_path_prefix in hooks/lib/project-root.sh that resolves symlinks only in the parent directory and preserves the basename verbatim. Symlinked project prefixes (e.g. /var vs /private/var) still match a canonical expected path, but a symlink at the leaf no longer impersonates the real filename.
  • Rewire is_cancel_authorized to use the prefix-only helper for both src and dest. Document the distinction in-place. Update the on-disk src_original probe to reference canonical_loop_dir so the symlink-rejection check runs against the real path.

Test plan

  • New HELPER TEST 9 covers the destination symlink alias attack
  • New HELPER TEST 10 covers the source symlink alias attack
  • Existing HELPER TEST 8 (symlinked prefix) still passes, proving prefix resolution still works
  • bash tests/run-all-tests.sh -> 1725 passed / 0 failed

Using realpath-with-leaf-dereference on the user-provided mv source and
destination lets a symlink aliasing <loop>/state.md or
<loop>/cancel-state.md pass authorization. For the destination case
the vulnerability is exploitable: `mv <loop>/state.md /tmp/link` (where
/tmp/link -> <loop>/cancel-state.md) canonicalizes to the expected
path and passes the check, but `mv` replaces the link at /tmp/link
rather than creating <loop>/cancel-state.md, corrupting loop state and
depositing state.md outside the loop dir.

Introduce canonicalize_path_prefix in project-root.sh that resolves
symlinks ONLY in the parent directory and preserves the basename
verbatim. Symlinked project prefixes (e.g. /var vs /private/var) still
match a canonical expected path, but a symlink at the leaf no longer
impersonates the real filename. Rewire is_cancel_authorized to use
the prefix-only helper for both src and dest, and document why the
distinction matters in-place. Update the on-disk src_original probe
to reference canonical_loop_dir so the symlink-rejection check runs
against the real path rather than any user-supplied non-canonical form.

Add two regression tests covering the destination and source symlink
alias attacks.
@SihaoLiu
Copy link
Copy Markdown
Contributor Author

@codex review this PR

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

The main FILE_PATH vs CORRECT_PATH comparison in loop-read-validator.sh
and loop-write-validator.sh previously used canonicalize_path, which
dereferences symlinks at the leaf. A planted symlink at the correct
filename inside the loop dir would then canonicalize to its target and
let the validator approve a Read or Write that follows the link out of
the loop dir, expanding Claude's effective file-access reach beyond
what the hook intends to permit.

Switch both validators to canonicalize_path_prefix so a symlinked
project ancestor still resolves correctly (the original bug this
feature fixes) while a symlink at the leaf no longer impersonates the
expected filename. This matches the same discipline applied to
is_cancel_authorized and preserves the intended semantics that the
basename is compared verbatim.

Methodology-analysis checks in these validators intentionally keep
full-path realpath because they use prefix-containment (path starts
with loop-dir/) rather than equality, which correctly catches symlinks
escaping the loop dir.
@SihaoLiu
Copy link
Copy Markdown
Contributor Author

@codex review this PR as entirety

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@SihaoLiu SihaoLiu merged commit 98d86c0 into dev Apr 17, 2026
9 checks passed
@SihaoLiu SihaoLiu mentioned this pull request Apr 21, 2026
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.

1 participant