Skip to content

CVE-2026-60004: Verified Reproduction

CVE-2026-60004: Gitea diffpatch Git hook installation leads to remote code execution

CVE-2026-60004 is verified against go-gitea/gitea · github. Affected versions: >=1.17, <1.27.1. Fixed in 1.27.1. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00312.

REPRO-2026-00312 go-gitea/gitea · github RCE Jul 29, 2026 CVE entry ↗ .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
30m 6s
Tool calls
243
Spend
$3.49
01 · Overview

What Is CVE-2026-60004?

CVE-2026-60004 is a critical-severity RCE vulnerability affecting go-gitea/gitea >=1.17, <1.27.1. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00312).

02 · Severity & CVSS

CVE-2026-60004 Severity

CVE-2026-60004 is rated critical severity.

CRITICAL threat level
Weakness CWE-94 (Improper Control of Generation of Code - Code Injection) — Improper Control of Generation of Code ('Code Injection')

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected go-gitea/gitea Versions

go-gitea/gitea · github versions >=1.17, <1.27.1 are affected.

How to Reproduce CVE-2026-60004

$ pruva-verify REPRO-2026-00312
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00312/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-60004

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

diffpatch patch content (Content field) submitted twice to POST /api/v1/repos/{owner}/{repo}/diffpatch, adding an executable hooks/post-index-change file

Attack chain
  1. Unauthenticated /user/sign_up registration
  2. create repo
  3. POST /api/v1/repos/{owner}/{repo}/diffpatch (call #1 clean apply, call #2 add/add collision + --3way three-way fallback checkout in bare temp clone)
  4. post-index-change Git hook executes as the Gitea OS user
Runnable proof: reproduction_steps.sh
Captured evidence: gitea fixed stdout
How the agent worked 556 events · 243 tool calls · 54 min
54 minDuration
243Tool calls
134Reasoning steps
556Events
17Dead-ends
Agent activity over 54 min
Policy
1
Support
21
Repro
234
Judge
94
Variant
202
0:0053:43

Root Cause and Exploit Chain for CVE-2026-60004

Versions: Gitea >= 1.17 and < 1.27.1.

Gitea's POST /api/v1/repos/{owner}/{repo}/diffpatch endpoint applies attacker-controlled patches inside a shared bare temporary clone (services/repository/files/patch.goTemporaryUploadRepository.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.

  • 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
    1. 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:

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.txtregistration_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.txtRCE_v1.27.0_CONFIRMED (written by the executed hook).
  • bundle/logs/repro/vuln_rce_hook.loghook_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.

CVE-2026-60004 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:13
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-60004 · REPRO-20
0:04
0:06
web search
0:28
0:29
0:31
web search
0:32
0:37
0:38
web search
0:39
0:44
0:46
web search
0:49
1:02
1:02
extract_facts
no facts extracted
1:04
1:04
supportclaim_contract
1:09
1:10
1:10
1:10
1:11
1:11
1:11
1:11
1:12
1:12
error

Unknown error

1:13
1:13
1:13
08 · How to Fix

How to Fix CVE-2026-60004

Upgrade go-gitea/gitea · github to 1.27.1 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-60004 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-60004

Is CVE-2026-60004 exploitable?

Yes. Pruva independently reproduced CVE-2026-60004 in go-gitea/gitea and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00312).

How severe is CVE-2026-60004?

CVE-2026-60004 is rated critical severity.

What type of vulnerability is CVE-2026-60004?

CVE-2026-60004 is classified as CWE-94 (Improper Control of Generation of Code - Code Injection) (Improper Control of Generation of Code ('Code Injection')), a RCE vulnerability.

Which versions of go-gitea/gitea are affected by CVE-2026-60004?

go-gitea/gitea >=1.17, <1.27.1 is affected by CVE-2026-60004.

Is there a fix for CVE-2026-60004?

Yes. CVE-2026-60004 is fixed in go-gitea/gitea 1.27.1. Upgrading to the fixed version remediates the issue.

How can I reproduce CVE-2026-60004?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-60004 reproduction verified?

Yes. Pruva reproduced CVE-2026-60004 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-60004

Authoritative sources for CVE-2026-60004 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.