~/en/research $ cat case-002.md
Gitea diffpatch: code execution from an account without administrator privileges
Reconstruction of the code injection through the Gitea diffpatch route, disclosed by the vendor with a working proof of concept. The case proves at process level who actually executes, measures the latency from an account without administrator privileges and publishes the false positive it measured in its own rules.
What was published and what this case does
Gitea published advisory GHSA-rcr6-4jqh-j84m on 28 July 2026 with a proof of concept that works, and the fix commit d7bc52be is dated 26 July, shipped in Gitea 1.27.1. NVD opened CVE-2026-60004 on 26 August with CWE-94 and a CVSS 3.1 of 9.8 that NVD itself marks as Secondary. In the official CISA KEV feed the entry has dateAdded 2026-08-25 and dueDate 2026-08-28, one day before NVD published the CVE.
The case discovers nothing: it takes that disclosure and rebuilds it in its own lab so a defender can see the chain, the closure against the fixed version, and the rules with their measured false positives. It is the first case produced end to end by the lab pipeline v2, where every phase gate is a counted script: the published-numbers verifier detected 26 injected corruptions in 26 attempts.
The chain, step by step
The entry point is the diffpatch route. A diff shaped like a patch lands in a temporary clone of the repository; in an add/add merge with a conflict, Git materializes the content and, if the temporary filesystem is writable and executable, a hook drops exactly where Git is going to invoke it. The real execution chain measured in the case is not the one the output suggests: the authentic parent is git write-tree, which runs /bin/sh hooks/post-index-change as uid=1000(git).
What makes this vulnerability awkward is its silence. Both requests answer HTTP 201 and the hook failure never propagates to the response, so the operator sees nothing unusual. The application log does not help either: across the case runs there is not a single line naming the hook execution.
And there is no need to be an administrator. The account used for the whole run was created through open registration, with no API token. It took between 0.82 and 0.89 seconds from that account to code execution, measured across three runs.
What the agent did and what it measured
Canary first, always: before any impact demonstration, execution is validated with a reversible canary, and the demonstration goes through an explicit human gate. Then A/B fix verification with a witness: 1.27.1 rejects the byte-identical payload in 22 of 22 attempts, with the monitor attesting that it reached disk and the vulnerable branch executing it that same day. Without a live positive control, a negative means nothing.
The same primitive measured the blast radius: app.ini readable, planted credentials read back, and arbitrary writes inside /data/gitea. What the case did not do was connect to any database or take away a database credential from a real system.
Its own contribution, independent of the advisory, is static: the 1.27.0 binary contains the string conflict detected at: git ls-files -u -z: %w. git ls-files -u lists the unmerged entries of an index, so that string is the collision detector of a three-way merge. Its presence in the diffpatch handler confirms the add/add mechanism described in the advisory without depending on it.
Where the agent stopped
This case publishes its bad measurements. designs/FACTS.md carries a section of discarded attempts and invalid measurements: the first grep -c diffpatch over /usr/local/bin/gitea returned 0 because that file is a 581-byte bash wrapper; the canary timestamp came out truncated because busybox does not expand %N; and a total_repos=0 was a quoting bug in the hook itself, so the repository count is not established.
The ab-structural.sh probe was declared unreliable and withdrawn as evidence: the temporary repository lives for milliseconds and the atomic checks were losing a race. And the first pcap simply did not exist, because tcpdump without sudo does not capture on the bridge; it was recaptured and the gap documented, because that first pcap would have been false evidence.
On detection, the case measured its own false positive: sid:202660041 fired in all three cycles of a benign control and is published with low confidence. There is also a limitation that tuning does not fix: the fix keeps git apply --index --cached -3, so detecting on diff content is fragile by construction. Still not established: the prevalence of installations exposing the three preconditions, real-world exploitation, and whether other mitigations break the chain.
Detection and remediation
Upgrade to 1.27.1 or later. Quick audit without touching the service: check the version (from 1.17 up to but not including 1.27.1), confirm the diffpatch route exists by reading the binary and not the documentation, and look from inside the container at whether the temporary filesystem is writable and executable. Removing exec from that mount or disabling the route breaks the chain, although the case tested only the vendor default.
The published rules cover three layers. On the web: an executable hook patch arriving at diffpatch, the same patch twice from the same source (the add/add collision), and a patch accepted with 201, which is the silent success. On the host: git write-tree spawning a shell hook and a hook file materializing inside the temporary Gitea clone. On the network and in artifacts: Suricata and YARA.
The detections fire from offline replay of the case pcaps, never against a live interface, and before taking them to production they have to be tuned against your own telemetry: the case says so literally, and the measured false positive is the concrete reason.
Results
- The diffpatch route executes code as the
gitservice: the real parent isgit write-tree, which runs/bin/sh hooks/post-index-changewithuid=1000(git). Proven at process level, not by output text. - The three advisory preconditions (Git >= 2.32, the diffpatch route enabled, and a temporary filesystem that is writable and executable) are met by default on the official
gitea/gitea:1.27.0image without touching configuration. - Measured delivery latency: 0.82 to 0.89 seconds across three runs, with both requests answering HTTP 201 (silent success: the hook failure does not propagate to the response) from an account without administrator privileges created through open registration.
- The 1.27.1 fix rejects the byte-identical payload: 22 of 22 attempts with no execution and the second diffpatch answering 500, with the monitor attesting that the payload reached disk and the vulnerable branch running the same day.
- Blast radius measured with the same primitive:
app.inireadable, planted credentials read back and arbitrary writes inside/data/gitea. Its own static finding, independent of the advisory: the 1.27.0 binary contains the stringconflict detected at: git ls-files -u -z: %w, the collision detector of a three-way merge, which confirms the described add/add mechanism.
you@nullsector:~$ mail miguel
Collaborations, talks, press or research: write to me.
Need a pentest or a vulnerability assessment? Request it at Xpectra.ai ↗