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
Security review of the worktree-isolation library layer merged in #12 (commit 1a9eb11). Found by an adversarial security pass (Codex + a second reviewer) against the threat model "a malicious or compromised Codex run, or a crafted repository, escapes the worktree sandbox / damages the host." The most serious findings were reproduced empirically, not theorized.
Context for severity:createWorktreeSession / cleanupWorktreeSession currently have no production callers (the library layer landed in #12; companion --worktree wiring is the next PR). So these are code-path severity once wired, not a live exploitable surface today. They must be closed before the --worktree flag reaches the CLI.
Findings
1. CRITICAL — .worktrees/ symlink traversal redirects worktree creation to an arbitrary host location
git.mjs createWorktree does fs.mkdirSync(path.join(repoRoot, ".worktrees"), { recursive: true }). Node's recursive mkdir follows a symlink at the final component. A crafted repo (or a co-resident user, or a prior malicious run that did rm -rf .worktrees && ln -s ~/.config .worktrees) causes the subsequent git worktree add to populate ~/.config/codex-<ts>/ — i.e. the Codex workspace-write root lands in ~/.ssh, ~/.config, ~/Library/LaunchAgents, etc. Reproduced end-to-end (.worktrees → /tmp/attacker-dest created /tmp/attacker-dest/codex-FAKE/; also the ~/.ssh variant).
2. HIGH — symlink-mode blob in the patch replays a host-file symlink into repoRoot
applyWorktreePatch captures the worktree diff with git diff --cached --binary --output and replays it with git apply --index in repoRoot. A symlink created in the worktree (ln -s ~/.ssh/authorized_keys ./steal) is captured as a mode-120000 entry with the target path verbatim, and git apply recreates it in repoRoot pointing at the host file. Subsequent cat/git show/editor-open exfiltrates; >> appends. The byte-preserving --output transfer does not help — the symlink target is legitimately part of the diff. Reproduced.
3. MEDIUM — git rev-parse --git-dir outside repoRoot → info/exclude append at an attacker-chosen path
createWorktree resolves git rev-parse --git-dir with path.resolve(repoRoot, rawGitDir). When --git-dir returns an absolute path (separate-git-dir, linked worktree, .git gitfile), path.resolve discards repoRoot and the subsequent appendFileSync(<gitDir>/info/exclude, ".worktrees/\n") writes to that absolute location. Content is benign (.worktrees/), but the missing boundary check is a structural gap. Partially reproduced (legit linked worktree); destructive chain needs a pre-staged valid git dir.
4. MEDIUM — unbounded worktree accumulation (disk DoS)
By design (leave-branch, #12) cleanupWorktreeSession never removes the worktree or branch. Every rescue call adds one .worktrees/codex-<ts>/ checkout + branch, forever. No cap, no roster, no warning. A rescue loop or long session fills disk (GB-per-call on a large repo).
Ruled out (checked, not exploitable)
Command injection — all git calls go through runCommand(..., {shell:false}) arg-array.
baseCommit / branch — plugin-generated (rev-parse HEAD SHA; codex/<timestamp>).
git apply path traversal via ../ in diff paths — git rejects even with --unsafe-paths (not passed).
Security review of the worktree-isolation library layer merged in #12 (commit 1a9eb11). Found by an adversarial security pass (Codex + a second reviewer) against the threat model "a malicious or compromised Codex run, or a crafted repository, escapes the worktree sandbox / damages the host." The most serious findings were reproduced empirically, not theorized.
Context for severity:
createWorktreeSession/cleanupWorktreeSessioncurrently have no production callers (the library layer landed in #12; companion--worktreewiring is the next PR). So these are code-path severity once wired, not a live exploitable surface today. They must be closed before the--worktreeflag reaches the CLI.Findings
1. CRITICAL —
.worktrees/symlink traversal redirects worktree creation to an arbitrary host locationgit.mjs createWorktreedoesfs.mkdirSync(path.join(repoRoot, ".worktrees"), { recursive: true }). Node's recursive mkdir follows a symlink at the final component. A crafted repo (or a co-resident user, or a prior malicious run that didrm -rf .worktrees && ln -s ~/.config .worktrees) causes the subsequentgit worktree addto populate~/.config/codex-<ts>/— i.e. the Codex workspace-write root lands in~/.ssh,~/.config,~/Library/LaunchAgents, etc. Reproduced end-to-end (.worktrees → /tmp/attacker-destcreated/tmp/attacker-dest/codex-FAKE/; also the~/.sshvariant).2. HIGH — symlink-mode blob in the patch replays a host-file symlink into repoRoot
applyWorktreePatchcaptures the worktree diff withgit diff --cached --binary --outputand replays it withgit apply --indexin repoRoot. A symlink created in the worktree (ln -s ~/.ssh/authorized_keys ./steal) is captured as a mode-120000 entry with the target path verbatim, andgit applyrecreates it in repoRoot pointing at the host file. Subsequentcat/git show/editor-open exfiltrates;>>appends. The byte-preserving--outputtransfer does not help — the symlink target is legitimately part of the diff. Reproduced.3. MEDIUM —
git rev-parse --git-diroutside repoRoot →info/excludeappend at an attacker-chosen pathcreateWorktreeresolvesgit rev-parse --git-dirwithpath.resolve(repoRoot, rawGitDir). When--git-dirreturns an absolute path (separate-git-dir, linked worktree,.gitgitfile),path.resolvediscards repoRoot and the subsequentappendFileSync(<gitDir>/info/exclude, ".worktrees/\n")writes to that absolute location. Content is benign (.worktrees/), but the missing boundary check is a structural gap. Partially reproduced (legit linked worktree); destructive chain needs a pre-staged valid git dir.4. MEDIUM — unbounded worktree accumulation (disk DoS)
By design (leave-branch, #12)
cleanupWorktreeSessionnever removes the worktree or branch. Every rescue call adds one.worktrees/codex-<ts>/checkout + branch, forever. No cap, no roster, no warning. A rescue loop or long session fills disk (GB-per-call on a large repo).Ruled out (checked, not exploitable)
runCommand(..., {shell:false})arg-array.baseCommit/branch— plugin-generated (rev-parse HEADSHA;codex/<timestamp>).git applypath traversal via../in diff paths — git rejects even with--unsafe-paths(not passed).Proposed fixes (for the security PR)
lstat-guard.worktrees/before mkdirSync (and reject symlinked components up the path) — closes Add model and effort flags to review commands #1. Single-line core.git apply— closes Supersedes #408: thread model + effort overrides through app-server spawn path (fixes #476) #2.path.resolve(gitDir), assertgitDiris contained in repoRoot (realpath-normalized), bail otherwise — closes Rebase fork on upstream #471 (RPC override) + cherry-pick shutdown hardening + #506/#480/#490 #3..worktrees/codex-*; warn loudly at N (e.g. 10) or fail-closed — closes Rebase fork on upstream #471 (RPC override) + #506/#480/#490 + shutdown hardening #4. Additive, preserves the leave-branch contract.Refs #12, fork#10, openai#135, openai#137.