Skip to content

security(high): symlink-mode blob in worktree patch replays a host-file symlink into repoRoot #15

Closed

Description

@axisrow

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

  1. Inside the worktree, create ln -s ~/.ssh/authorized_keys ./steal (or any host path), git add -A.
  2. git diff --cached --binary HEAD --output <patch> captures ./steal with mode 120000 and target /Users/.../.ssh/authorized_keys.
  3. 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.

Activity

  1. added a commit that references this issue on Jul 19, 2026
    a6658d8
  2. axisrow commented on Jul 20, 2026

    @axisrow
    OwnerAuthor

    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.

  3. added 3 commits that reference this issue on Jul 20, 2026
    eb3f692
    579f6b6
    3bbf13c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions