You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security(high): symlink-mode blob in worktree patch replays a host-file symlink into repoRoot #15
Security finding from the adversarial review of #12 (commit 1a9eb11). Reproduced empirically.
Vulnerability
applyWorktreePatch (plugins/codex/scripts/lib/git.mjs) captures the worktree diff with git diff --cached --binary --output=<patch> and replays it with git apply --index <patch> in repoRoot. A symlink created inside the (attacker/Codex-controlled) worktree is captured as a mode-120000 diff entry with the target path stored verbatim, and git apply --index recreates it in repoRoot pointing at the attacker-chosen absolute host path.
The byte-preserving --output transfer does not help — the symlink target is legitimately part of the diff content.
Attack
Inside the worktree, create ln -s ~/.ssh/authorized_keys ./steal (or any host path), git add -A.
git diff --cached --binary HEAD --output <patch> captures ./steal with mode 120000 and target /Users/.../.ssh/authorized_keys.
git apply --index <patch> in repoRoot recreates <repoRoot>/steal -> ~/.ssh/authorized_keys.
Once the symlink exists in repoRoot, any subsequent cat ./steal, git show :./steal, editor-open, or git add of the path lets the attacker read the host file; with >>, append.
Reproduced exactly: after apply, <repoRoot>/authorized_keys_link -> /Users/axisrow/.ssh/authorized_keys existed.
Note: git apply rejects patches with literal ../ filename paths, but symlink-mode entries bypass that guard because the repo-side filename is a normal path — only the symlink target points outside.
Severity
HIGH once wired (no production callers today). Exfiltration + append primitive via the keep-transfer path.
Fix
After capture but before git apply, reject the patch if any added blob has mode 120000 (symlink). Alternatively, post-apply scan repoRoot for newly-created symlinks and abort. git apply --no-symlink-refs does not exist as a portable flag, so the pre-apply mode check is the reliable guard.
Reopening — the merged fix (#19) closes the NEW-symlink case but Codex's review found a bypass: an EXISTING tracked symlink that Codex retargets emits an index <old>..<new> 120000 header (no new file mode line), which the line-anchored regex misses. git apply then recreates the retargeted symlink in repoRoot pointing at the attacker-chosen host path.
Reproduced empirically: a tracked symlink link -> /tmp/benign, retargeted to -> /etc/passwd, produces a patch with index d0a3aa8..3594a94 120000 and NO mode header; git apply --index recreates link -> /etc/passwd in the target tree.
Follow-up fix needed: structural mode validation covering both new file mode 120000 / old|new mode 120000 AND the unchanged-mode index ... 120000 header. Codex also suggests parsing git header records instead of substring-scanning (more robust), and passing --no-ext-diff/--no-textconv to pin patch generation. Tracking in a follow-up PR.
Activity
fix(security): reject symlink-mode blobs in the worktree patch
axisrow commentedon Jul 20, 2026
Reopening — the merged fix (#19) closes the NEW-symlink case but Codex's review found a bypass: an EXISTING tracked symlink that Codex retargets emits an
index <old>..<new> 120000header (nonew file modeline), which the line-anchored regex misses.git applythen recreates the retargeted symlink in repoRoot pointing at the attacker-chosen host path.Reproduced empirically: a tracked symlink
link -> /tmp/benign, retargeted to-> /etc/passwd, produces a patch withindex d0a3aa8..3594a94 120000and NO mode header;git apply --indexrecreateslink -> /etc/passwdin the target tree.Follow-up fix needed: structural mode validation covering both
new file mode 120000/old|new mode 120000AND the unchanged-modeindex ... 120000header. Codex also suggests parsing git header records instead of substring-scanning (more robust), and passing--no-ext-diff/--no-textconvto pin patch generation. Tracking in a follow-up PR.fix(security): frozen-tree symlink detection (TOCTOU-closed, closes #15)
fix(security): neutralize symlink replay via core.symlinks=false (closes
fix(security): neutralize symlink replay via core.symlinks=false (closes