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.
Refs #12, #13 (umbrella), openai#135, openai#137.
Security finding from the adversarial review of #12 (commit 1a9eb11). Reproduced empirically.
Vulnerability
applyWorktreePatch(plugins/codex/scripts/lib/git.mjs) captures the worktree diff withgit diff --cached --binary --output=<patch>and replays it withgit apply --index <patch>in repoRoot. A symlink created inside the (attacker/Codex-controlled) worktree is captured as a mode-120000diff entry with the target path stored verbatim, andgit apply --indexrecreates it in repoRoot pointing at the attacker-chosen absolute host path.The byte-preserving
--outputtransfer does not help — the symlink target is legitimately part of the diff content.Attack
ln -s ~/.ssh/authorized_keys ./steal(or any host path),git add -A.git diff --cached --binary HEAD --output <patch>captures./stealwithmode 120000and 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, orgit addof the path lets the attacker read the host file; with>>, append.Reproduced exactly: after apply,
<repoRoot>/authorized_keys_link -> /Users/axisrow/.ssh/authorized_keysexisted.Note:
git applyrejects 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 mode120000(symlink). Alternatively, post-apply scan repoRoot for newly-created symlinks and abort.git apply --no-symlink-refsdoes not exist as a portable flag, so the pre-apply mode check is the reliable guard.Refs #12, #13 (umbrella), openai#135, openai#137.