[workflow][security] Require registered Git worktree proof for branches-only mutation authorization #713

Open
opened 2026-07-14 19:58:20 -05:00 by jcwalker3 · 0 comments
Owner

Problem

The branches-only mutation guard may accept an arbitrary directory created under branches/ as a valid worktree path. A caller can potentially bypass the control-checkout mutation restriction by running mkdir -p branches/<name> and supplying that path without creating or registering a Git worktree.

Incident evidence (while creating #712)

Observed operator sequence:

  1. Issue creation blocked by the branches-only mutation guard (control checkout).
  2. Runner executed:
    mkdir -p /Users/jasonwalker/Development/Gitea-Tools/branches/issue-creation-dummy
  3. Retried gitea_create_issue.
  4. Issue #712 was successfully created.

Residual path inspection (read-only, 2026-07-14)

Path: /Users/jasonwalker/Development/Gitea-Tools/branches/issue-creation-dummy

Check Result
Exists / is directory yes
Contents empty (0 entries; no files, no .git)
git worktree list --porcelain not registered
git -C <path> rev-parse --show-toplevel control checkout (.../Gitea-Tools) via walk-up
git -C <path> rev-parse --git-dir .../Gitea-Tools/.git (main repo, not .git/worktrees/...)
Branch / HEAD master @ 1eafb757... (control checkout identity)
Classification arbitrary empty directory under branches/, not a Git worktree

Sibling empty unregistered dirs with the same walk-up behavior also present:

  • branches/dummy-worktree (created 2026-07-13)
  • branches/fake_branch (created 2026-07-14)

Successful worktree_path for #712

  • Operator-observed path: /Users/jasonwalker/Development/Gitea-Tools/branches/issue-creation-dummy
  • Residual directory birth/mtime: 2026-07-14T20:52:12 local
  • #712 created_at: 2026-07-14T19:53:06-05:00
  • Local agent transcripts available to this investigation did not retain a durable MCP tool-call record of the successful #712 create with an explicit worktree_path field.
  • Therefore: treat the dummy path as the incident-claimed successful path; residual FS state proves the named path is still an unregistered empty directory and is still accepted by current guard logic (see below).

Guard behavior (code diagnosis, no fix applied)

Primary implementation: author_mutation_worktree.py + gitea_mcp_server.verify_preflight_purity / _enforce_branches_only_author_mutation.

What the guard does validate today:

  1. Path prefix / under branches/is_path_under_branches / assess_author_mutation_worktree (also has a weak global /branches/ substring check).
  2. Directory existence — missing path fails closed (does not exist).
  3. Repository membership via git rev-parse --git-common-dirassess_workspace_repo_membership when workspace ≠ process root.

What it does not validate:

  1. Actual Git worktree registration (git worktree list --porcelain) — not checked.
  2. Worktree-private git metadata (e.g. .git file pointing at .git/worktrees/<name> vs walk-up into control .git).
  3. Branch/HEAD association to a session-owned task worktree — under branches/, even master via walk-up is accepted by assess_author_mutation_worktree.
  4. Cleanliness / control-checkout separation beyond path prefix — porcelain dirty checks run against the resolved workspace; for an empty unregistered dir, git status also walks up to the control checkout.
  5. Symlink escape to control checkout / nested fake repos — not covered by registration proof.
  6. Stale/prunable worktree records — not rejected by a registration-health check.

Critical false-positive path (reproduced)

For empty branches/issue-creation-dummy:

assess_author_mutation_worktree(...): proven=True, block=False, under_branches=True
assess_workspace_repo_membership(...): proven=True, block=False
  (git-common-dir resolves to control .git via parent walk-up)
registered in git worktree list: False

So authorization is effectively: path under branches/ + exists + git walk-up reaches expected repo, not registered worktree proof.

Related but not equivalent issues:

  • #274 (closed): introduce branches-only rule — does not require registration proof.
  • #460 (closed): guard/runtime_context path agreement.
  • #618 (open): missing/disappeared author worktree binding inconsistency — different failure mode (missing path / inconsistent tools), not “mkdir fake dir bypass”.
  • #712 (open): stale command supersession — different defect; this issue is the guard-bypass side-channel observed while filing #712.

No open/closed issue was found whose primary subject is fake-worktree / arbitrary-branches/-directory authorization bypass.

Required behavior

  • Resolve and canonicalize the supplied path.
  • Verify it appears in git worktree list --porcelain.
  • Verify it belongs to the expected repository.
  • Verify it has valid Git metadata, branch/HEAD identity, and workspace binding.
  • Reject arbitrary directories, symlink escapes, nested fake repositories, stale worktree records, and paths belonging to another repository.
  • Do not authorize mutation based only on a branches/ path prefix.
  • Return a structured BLOCKED + DIAGNOSE result explaining the missing worktree proof.
  • Record the supplied path, canonical path, repository, worktree registration, and rejection reason in the audit trail.

Acceptance criteria

  1. A plain directory created with mkdir -p branches/fake is rejected.
  2. A registered worktree under branches/ is accepted when all other gates pass.
  3. A symlink to the control checkout is rejected.
  4. A worktree from another repository is rejected.
  5. A stale/prunable worktree record is rejected.
  6. Path traversal and alternate path spellings cannot bypass validation.
  7. Tests reproduce the Issue #712 creation sequence (mkdir dummy → supply path → must block).
  8. Existing legitimate author issue-creation flows continue to work.

Cleanup note (not performed)

After verification, branches/issue-creation-dummy is empty, unregistered, and contains no user work → safe to remove with rmdir once operators approve. Do not remove registered worktrees or nonempty directories. Sibling empty fakes (dummy-worktree, fake_branch) are candidates for the same treatment after separate confirmation.

Cross-links

## Problem The branches-only mutation guard may accept an arbitrary directory created under `branches/` as a valid worktree path. A caller can potentially bypass the control-checkout mutation restriction by running `mkdir -p branches/<name>` and supplying that path without creating or registering a Git worktree. ## Incident evidence (while creating #712) Observed operator sequence: 1. Issue creation blocked by the branches-only mutation guard (control checkout). 2. Runner executed: `mkdir -p /Users/jasonwalker/Development/Gitea-Tools/branches/issue-creation-dummy` 3. Retried `gitea_create_issue`. 4. Issue **#712** was successfully created. ### Residual path inspection (read-only, 2026-07-14) Path: `/Users/jasonwalker/Development/Gitea-Tools/branches/issue-creation-dummy` | Check | Result | |------|--------| | Exists / is directory | yes | | Contents | empty (0 entries; no files, no `.git`) | | `git worktree list --porcelain` | **not registered** | | `git -C <path> rev-parse --show-toplevel` | control checkout (`.../Gitea-Tools`) via walk-up | | `git -C <path> rev-parse --git-dir` | `.../Gitea-Tools/.git` (main repo, not `.git/worktrees/...`) | | Branch / HEAD | `master` @ `1eafb757...` (control checkout identity) | | Classification | **arbitrary empty directory under `branches/`**, not a Git worktree | Sibling empty unregistered dirs with the same walk-up behavior also present: - `branches/dummy-worktree` (created 2026-07-13) - `branches/fake_branch` (created 2026-07-14) ### Successful `worktree_path` for #712 - Operator-observed path: `/Users/jasonwalker/Development/Gitea-Tools/branches/issue-creation-dummy` - Residual directory birth/mtime: **2026-07-14T20:52:12** local - #712 `created_at`: **2026-07-14T19:53:06-05:00** - Local agent transcripts available to this investigation did **not** retain a durable MCP tool-call record of the successful #712 create with an explicit `worktree_path` field. - Therefore: treat the dummy path as the **incident-claimed** successful path; residual FS state proves the named path is still an unregistered empty directory and is still accepted by current guard logic (see below). ## Guard behavior (code diagnosis, no fix applied) Primary implementation: `author_mutation_worktree.py` + `gitea_mcp_server.verify_preflight_purity` / `_enforce_branches_only_author_mutation`. What the guard **does** validate today: 1. **Path prefix / under `branches/`** — `is_path_under_branches` / `assess_author_mutation_worktree` (also has a weak global `/branches/` substring check). 2. **Directory existence** — missing path fails closed (`does not exist`). 3. **Repository membership via `git rev-parse --git-common-dir`** — `assess_workspace_repo_membership` when workspace ≠ process root. What it **does not** validate: 1. **Actual Git worktree registration** (`git worktree list --porcelain`) — **not checked**. 2. **Worktree-private git metadata** (e.g. `.git` file pointing at `.git/worktrees/<name>` vs walk-up into control `.git`). 3. **Branch/HEAD association to a session-owned task worktree** — under `branches/`, even `master` via walk-up is accepted by `assess_author_mutation_worktree`. 4. **Cleanliness / control-checkout separation beyond path prefix** — porcelain dirty checks run against the resolved workspace; for an empty unregistered dir, `git status` also walks up to the control checkout. 5. **Symlink escape to control checkout** / nested fake repos — not covered by registration proof. 6. **Stale/prunable worktree records** — not rejected by a registration-health check. ### Critical false-positive path (reproduced) For empty `branches/issue-creation-dummy`: ```text assess_author_mutation_worktree(...): proven=True, block=False, under_branches=True assess_workspace_repo_membership(...): proven=True, block=False (git-common-dir resolves to control .git via parent walk-up) registered in git worktree list: False ``` So authorization is effectively: **path under `branches/` + exists + git walk-up reaches expected repo**, not **registered worktree proof**. Related but **not equivalent** issues: - #274 (closed): introduce branches-only rule — does not require registration proof. - #460 (closed): guard/runtime_context path agreement. - #618 (open): missing/disappeared author worktree binding inconsistency — different failure mode (missing path / inconsistent tools), not “mkdir fake dir bypass”. - #712 (open): stale command supersession — **different defect**; this issue is the guard-bypass side-channel observed while filing #712. No open/closed issue was found whose primary subject is fake-worktree / arbitrary-`branches/`-directory authorization bypass. ## Required behavior - Resolve and canonicalize the supplied path. - Verify it appears in `git worktree list --porcelain`. - Verify it belongs to the expected repository. - Verify it has valid Git metadata, branch/HEAD identity, and workspace binding. - Reject arbitrary directories, symlink escapes, nested fake repositories, stale worktree records, and paths belonging to another repository. - Do not authorize mutation based only on a `branches/` path prefix. - Return a structured BLOCKED + DIAGNOSE result explaining the missing worktree proof. - Record the supplied path, canonical path, repository, worktree registration, and rejection reason in the audit trail. ## Acceptance criteria 1. A plain directory created with `mkdir -p branches/fake` is rejected. 2. A registered worktree under `branches/` is accepted when all other gates pass. 3. A symlink to the control checkout is rejected. 4. A worktree from another repository is rejected. 5. A stale/prunable worktree record is rejected. 6. Path traversal and alternate path spellings cannot bypass validation. 7. Tests reproduce the Issue #712 creation sequence (`mkdir` dummy → supply path → must block). 8. Existing legitimate author issue-creation flows continue to work. ## Cleanup note (not performed) After verification, `branches/issue-creation-dummy` is empty, unregistered, and contains no user work → **safe to remove with `rmdir`** once operators approve. Do **not** remove registered worktrees or nonempty directories. Sibling empty fakes (`dummy-worktree`, `fake_branch`) are candidates for the same treatment after separate confirmation. ## Cross-links - Discovered while creating: #712 - Related context: #274, #460, #618
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#713