# REPRO-2026-00312: Gitea diffpatch Git hook installation leads to remote code execution ## Summary Status: published Severity: critical CVSS: Unknown CWE: CWE-94 (Improper Control of Generation of Code - Code Injection) (Improper Control of Generation of Code ('Code Injection')) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00312 CVE: CVE-2026-60004 ## Package Name: go-gitea/gitea Ecosystem: github Affected: >=1.17, <1.27.1 Fixed: 1.27.1 ## Root Cause # RCA Report — CVE-2026-60004 / GHSA-rcr6-4jqh-j84m ## Summary Gitea's `POST /api/v1/repos/{owner}/{repo}/diffpatch` endpoint applies attacker-controlled patches inside a **shared bare** temporary clone (`services/repository/files/patch.go` → `TemporaryUploadRepository.Clone(..., bare=true)`). Because the clone is bare, its repository root *is* `$GIT_DIR`. Submitting the same patch twice creates an add/add collision; git's `-3` three-way fallback (enabled for Git ≥ 2.32) then **checks the indexed path out to the working tree** even though the operation uses `--cached`. An executable file placed at `hooks/post-index-change` therefore lands in the live Git hooks directory and becomes an active hook. Git executes it while writing the index, so repository-controlled content runs arbitrary shell commands as the Gitea OS user. With default open registration an unauthenticated visitor can obtain the required write access by registering an account and creating a repository. ## Impact - **Package/component affected:** `services/repository/files/patch.go` (`ApplyDiffPatch`), reached via the public REST endpoint `POST /api/v1/repos/{owner}/{repo}/diffpatch` and the web editor's "apply patch" / cherry-pick fallback paths. Also affects `services/repository/files/cherry_pick.go`. - **Affected versions:** Gitea `>= 1.17` and `< 1.27.1`. - **Risk level:** Critical — remote code execution as the Gitea service account. With open registration (default) the endpoint is reachable by an unauthenticated attacker who self-registers. ## Impact Parity - **Disclosed/claimed maximum impact:** Remote code execution (arbitrary shell command execution as the Gitea OS user), reachable by an unauthenticated attacker via open registration. - **Reproduced impact from this run:** Full remote code execution. The planted `post-index-change` Git hook executed as the Gitea OS user (`vscode` in the test runtime) and wrote a marker file (`RCE_v1.27.0_CONFIRMED`) to disk, reached through the real `POST /api/v1/repos/{owner}/{repo}/diffpatch` endpoint after an unauthenticated self-registration (`/user/sign_up` → HTTP 303) and an ordinary repo-creation flow. - **Parity:** `full` — the claimed unauthenticated→account→repo→RCE chain was exercised end-to-end against the real product binary, with a fixed-version (v1.27.1) negative control that does **not** execute the hook. - **Not demonstrated:** Nothing; the code-execution outcome itself was demonstrated (not merely a crash). ## Root Cause `ApplyDiffPatch` prepares the patch in a temporary clone via `TemporaryUploadRepository.Clone(ctx, opts.OldBranch, /*bare=*/true)` with `Shared: true`. The apply command is: ```go cmdApply := gitcmd.NewCommand("apply", "--index", "--recount", "--cached", "--ignore-whitespace", "--whitespace=fix", "--binary") if git.DefaultFeatures().CheckVersionAtLeast("2.32") { cmdApply.AddArguments("-3") // --3way fallback } ``` Step-by-step: 1. **Call #1** — a clean patch that adds `hooks/post-index-change` (mode `100755`). With `--cached` the file only enters the index; no working-tree file is written. The resulting commit (tree containing `hooks/post-index-change`) is pushed to the repository, so `HEAD` now tracks that path. 2. **Call #2** — the *same* patch is submitted again. `SetDefaultIndex` (`git read-tree HEAD`) loads the index from the new `HEAD`, which already contains `hooks/post-index-change`. Re-applying the "add" patch produces an **add/add collision**. The `-3` three-way fallback then checks the indexed path out to the working tree even though `--cached` was requested. In a **bare** clone the working tree *is* `$GIT_DIR`, so the executable file is written to `$GIT_DIR/hooks/post-index-change` — a live Git hook. 3. While git writes the index during the apply/merge, it invokes the `post-index-change` hook, which executes the attacker's shell commands as the Gitea OS user. The hook's exit value is not propagated to the diffpatch HTTP response. **Fix (v1.27.1, PR #38637/#38638 "refactor: git patch apply"):** the temporary clone is no longer bare — `Clone(ctx, opts.OldBranch, /*bare=*/false)`. With a real working tree, the three-way fallback writes the checked-out path into the worktree (not `$GIT_DIR/hooks`), so no hook is installed and the apply fails closed (`git apply error: ... hooks/post-index-change: patch does not apply`, HTTP 500) instead of executing attacker code. ## Reproduction Steps 1. Reference: `bundle/repro/reproduction_steps.sh` (self-contained; downloads the official Gitea linux-amd64 binaries for the vulnerable `1.27.0` and the fixed `1.27.1` builds). 2. What the script does, for each build: - Starts a fresh Gitea instance (SQLite, open registration) on localhost as the current OS user and waits for the `/api/v1/version` healthcheck. - **Unauthenticated step:** `GET /user/sign_up` (verifies the open-registration form is served) then `POST /user/sign_up` to self-register an account (HTTP 303 = success); confirms the new account authenticates via the API (`GET /api/v1/user` with basic auth → HTTP 200). - Creates a repository `exploit-repo` with `auto_init` (establishes the `main` branch). - Builds a malicious patch that adds an executable file `hooks/post-index-change` whose body writes a version-specific marker file and records `id -un`. - **Call #1:** `POST /api/v1/repos/{owner}/exploit-repo/diffpatch` with the patch (clean apply, HTTP 201). - **Call #2:** the *same* patch again (add/add collision → three-way fallback → hook planted and triggered). - Checks for the RCE marker file (written by the hook as the Gitea OS user). 3. Expected evidence: on the vulnerable build the marker file `RCE_v1.27.0_CONFIRMED` is created and `vuln_rce_hook.log` records `hook_ran_as_user=vscode`; on the fixed build Call #2 returns HTTP 500 with `git apply error: ... hooks/post-index-change: patch does not apply` and no marker is created. ## Evidence All artifacts under `bundle/` (relative to the bundle root): - `bundle/repro/reproduction_steps.sh` — the reproducer. - `bundle/repro/runtime_manifest.json` — runtime manifest (entrypoint_kind `endpoint`, service_started/healthcheck_passed/target_path_reached all true). - `bundle/logs/repro/gitea_vuln_stdout.log` / `gitea_fixed_stdout.log` — Gitea server logs for each build. - `bundle/logs/repro/vuln_registration.txt` — `registration_http=303`, `api_auth_http=200` (unauthenticated→account chain). - `bundle/logs/repro/vuln_signup_page.html` — the served open-registration form. - `bundle/logs/repro/vuln_patch.txt` — the malicious patch payload. - `bundle/logs/repro/vuln_call1_response.json` / `vuln_call2_response.json` — diffpatch API responses (both HTTP 201 on the vulnerable build). - `bundle/logs/repro/vuln_diffpatch_calls.txt`: `call1_http=201 call2_http=201 marker_found=yes gitea_run_user=vscode`. - `bundle/logs/repro/vuln_rce_marker.txt` — `RCE_v1.27.0_CONFIRMED` (written by the executed hook). - `bundle/logs/repro/vuln_rce_hook.log` — `hook_ran_as_user=vscode` (twice, once per index write). - `bundle/logs/repro/fixed_call2_response.json` — `{"message":"git apply error: exit status 1 - Performing three-way merge... error: hooks/post-index-change: does not match index ... patch does not apply"}` (HTTP 500, hook NOT installed). - `bundle/logs/repro/fixed_diffpatch_calls.txt`: `call1_http=201 call2_http=500 marker_found=no`. Environment: official Gitea `1.27.0` / `1.27.1` linux-amd64 binaries, SQLite backend, Git 2.55.0 (≥ 2.32, so `-3` three-way fallback active), x86_64 Linux, gitea running as the `vscode` OS user. Key excerpts: ``` [vuln] Registration POST HTTP=303 (303 redirect = success) [vuln] API basic-auth as intruder_vuln: HTTP=200 [vuln] Call #1 HTTP=201 [vuln] Call #2 HTTP=201 [vuln] *** RCE MARKER FILE CREATED BY GIT HOOK *** [vuln] marker content: RCE_v1.27.0_CONFIRMED vuln_rce_hook.log: hook_ran_as_user=vscode [fixed] Call #2 HTTP=500 [fixed] No RCE marker file present at /tmp/gitea_rce_marker_v1.27.1 fixed_call2_response.json: "git apply error: exit status 1 - Performing three-way merge... error: hooks/post-index-change: does not match index ... patch does not apply" ``` ## Recommendations / Next Steps - **Upgrade to Gitea 1.27.1** (or later), which makes the temporary patch clone non-bare so checked-out paths cannot land in `$GIT_DIR/hooks`. - Defense-in-depth: do not run `git apply --index` against a bare repository at all; avoid combining `--cached` with `--index`/`--3way` semantics on a bare clone; consider `core.hooksPath` isolation / disabling `post-index-change` for internal temporary clones. - Restrict `service.DISABLE_REGISTRATION` / require admin approval on internet-facing instances to remove the unauthenticated reachability path. - Add a regression test asserting that the temporary patch repository is non-bare (the upstream fix added `services/repository/files/patch_test.go` `TestGitPatchPrepare` checking for `basePath/.git`). ## Additional Notes - **Idempotency:** confirmed — the script was run twice consecutively; both runs confirmed the vulnerable build (marker created, hook ran as the gitea OS user) and cleared the fixed build (no marker). The script removes prior per-run state and markers at start, so it is safe to re-run. - The `post-index-change` hook was introduced in Git 2.31/2.32; the `-3` three-way fallback requires Git ≥ 2.32 (gated by `git.DefaultFeatures().CheckVersionAtLeast("2.32")`). The test runtime uses Git 2.55.0, satisfying both. - The hook's exit value is not reflected in the diffpatch HTTP response, so the proof relies on the on-disk marker file and the `hook_ran_as_user` log rather than the API status code (Call #2 returns 201 on the vulnerable build). - The temporary upload repository is cleaned up by Gitea after the operation, so the planted hook file is transient; the durable proof is the marker the hook wrote while it was live. ## Reproduction Details Reproduced: 2026-07-29T10:56:28.309Z Duration: 1806 seconds Tool calls: 243 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00312 pruva-verify CVE-2026-60004 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00312&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00312/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-60004 - Source: https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 16297 bytes) - bundle/repro/rca_report.md (analysis, 10163 bytes) - bundle/repro/validation_verdict.json (other, 1001 bytes) - bundle/repro/runtime_manifest.json (other, 1194 bytes) - bundle/logs/repro/fixed_diffpatch_calls.txt (other, 132 bytes) - bundle/logs/repro/fixed_call2_response.json (other, 296 bytes) - bundle/logs/repro/vuln_registration.txt (other, 40 bytes) - bundle/logs/repro/vuln_patch.txt (other, 315 bytes) - bundle/logs/repro/gitea_vuln_stdout.log (log, 7459 bytes) - bundle/logs/repro/gitea_fixed_stdout.log (log, 7104 bytes) - bundle/logs/repro/vuln_call1_response.json (other, 1112 bytes) - bundle/logs/repro/fixed_call1_response.json (other, 1120 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00312 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00312/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/reproductions/REPRO-2026-00312 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev