# FACT SHEET — single source of truth (CVE-2026-60004) **Rule:** nothing goes into the paper, the video or the README unless it is here. What is not here does not exist, or is explicitly labeled "hypothesis" or "not confirmed". Dates read from the source, never remembered. Verified by the orchestrator unless marked otherwise. ## 1. Identity and public registry | Item | Value | Source + date | |---|---|---| | CVE | CVE-2026-60004 | NVD, consulted 2026-08-27 | | GHSA | GHSA-rcr6-4jqh-j84m (repo-scoped `go-gitea/gitea`; the global endpoint `api.github.com/advisories/` returns **404**) | GitHub API 2026-08-27 | | Title | "Remote Code Execution via diffpatch Git Hook Installation" | GHSA | | Affected range | `>=1.17, <1.27.1` | GHSA `vulnerable_version_range` | | First patched | **1.27.1** | GHSA `patched_versions` | | Official CVSS | `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H` = **9.8 CRITICAL** (*Secondary* type) | **NVD, measured by the parent** 2026-08-27 | | CWE | CWE-94 | GHSA + NVD | | Advisory published | **2026-07-28T17:34:46Z** (updated 18:40:54Z) | GHSA `published_at` | | NVD status | **Analyzed**, `published` **2026-08-26T20:17:56.010Z**, `lastModified` 2026-08-27T11:41:19.230Z | `designs/nvd-record.json` (verbatim API response) | | CISA KEV dateAdded | **2026-08-25** (`dueDate` 2026-08-28, `knownRansomwareCampaignUse = Unknown`, CWE-94) — the KEV entry is dated one calendar day BEFORE NVD published the CVE (`2026-08-26T20:17:56Z`): between one and two days earlier, because KEV records day granularity only. **Do not read `catalogVersion` (`2026.08.26` / `2026.08.27`) as the entry date** - that confusion is exactly what published a wrong KEV date in an earlier draft. | `designs/kev-record.json` — verbatim entry, fetched 2026-08-27 from catalog `2026.08.27` (1685 entries, full-file sha256 pinned in that file). **Correction 2026-08-27:** an earlier note here recorded `2026-08-26`; that was the *catalog version* (`2026.08.26`), not the entry's `dateAdded`. NVD publication really was 2026-08-26, i.e. **KEV entry preceded the NVD record by one day** | | CISA KEV dueDate | **2026-08-28** | ibid. | | Ransomware | `Unknown` (do not claim ransomware use) | ibid. | | BOD | The KEV note links **BOD 26-04** (3/14/60-day windows) | KEV `notes` | | CVSS v4.0 | `null` in NVD (do not claim) | NVD | ## 2. THE CENTRAL ETHICAL FACT **The vendor published the working exploit inside the advisory itself** (a complete ~330-line Python script, including an egress-free exfiltration technique via Git objects + a branch retrieved by smart HTTP). **We do not claim discovery anywhere.** The value of this work is: independent reproduction with process-level proof, A/B verification of the fix with a live negative, defender detections, and the empirical verification of the preconditions the vendor only asserts. ## 3. Mechanism - Endpoint: `POST /api/v1/repos/{owner}/{repo}/diffpatch`. - The attacker's patch is applied onto a **shared bare temporary clone** in `/data/gitea/tmp/local-repo/upload.git`. - Command: `git apply --index --recount --cached --binary`, and `-3` if git ≥2.32. - Sending the **same patch twice** ⇒ add/add collision ⇒ the three-way fallback extracts the path to the working tree **despite `--cached`**. - In a bare repo the root **is** `$GIT_DIR` ⇒ `hooks/post-index-change` (mode `100755`) is a live hook ⇒ Git invokes it when writing the index ⇒ execution as the OS user `git`. - **The hook's return value does NOT propagate into the `diffpatch` response** (the attack is silent in the HTTP response). ## 4. Fix (real commit, obtained by the parent) `d7bc52beeadff4be5f5690de4d5de42abd10affe` — 2026-07-26T17:32:02Z — "refactor: git patch apply (#38637) (#38638)" — files: `cherry_pick.go`, `patch.go`, `patch_test.go`, `update.go`. Diff in `designs/fix-commit-full.diff`. The decisive change: ```go - if err := t.Clone(ctx, opts.OldBranch, true); err != nil { + // here must NOT use bare repo, because the following git commands might operate working tree ("--index") directly + if err := t.Clone(ctx, opts.OldBranch, false); err != nil { ``` **An important and non-obvious consequence:** the fix does **not** touch the `git apply` flags nor blacklist paths. It only separates the worktree from `$GIT_DIR`. A patch *byte for byte identical* to the exploit's becomes inert. **Therefore detection based only on the patch content is structurally weak: you have to detect behavior (a hook appearing / process lineage), not only the diff's signature.** ## 5. Laboratory (built and verified by the parent) - Docker 28.5.2. Network `cve60004_lab`, `--internal`, subnet `172.31.224.0/24`, host gateway `172.31.224.1` (`br-403a9e21faf9`) = the attacker's position. - `gitea-vuln` = `gitea/gitea:1.27.0` at `172.31.224.2:3000`; `gitea-patched` = `1.27.1` at `172.31.224.3:3000`. Fictional hostname `gitea.lab`. `/data` volume. - **Zero egress verified:** `wget http://1.1.1.1` → `Network unreachable`; `https://api.github.com` → `bad address` (DNS dead). The only connections during inference and attack are to the LAN. - Local images: 1.27.0, 1.27.1, 1.27.2. - Non-admin user `labattacker` created **via open registration** (see §7). - Planted secrets (fictional): `/data/gitea/conf/planted-secrets.env`, `/data/canary/NONCE.txt`. ## 6. The vendor's preconditions — EMPIRICALLY VERIFIED (our own contribution) | Precondition (verbatim from the advisory) | Verdict | Evidence | |---|---|---| | Git ≥ 2.32 | ✅ | `git version 2.54.0` in 1.27.0 **and** 1.27.1 | | The `diffpatch` route enabled | ✅ | binary `/app/gitea/gitea` (124 MB): `/repos/%s/%s/diffpatch`, `ApplyRepoDiffPatch`, `routers/api/v1/repo.ApplyDiffPatch` | | tmpfs writable **and** executable | ✅ | script executed from `/tmp` → `TMP_EXEC_OK` | **Derived finding:** in the default official image **all three hold without setting any precondition-related option**. The argument "it only exploits if you misconfigure it" falls. ## 7. The credential-free path `GET /user/sign_up` → HTTP 200. `POST /user/sign_up` with `{user_name,email,password,retype}` (no prior authenticated cookie, `_csrf` sent empty) → **HTTP 303** and the account `labattacker` created, `IsActive true`, `IsAdmin false`. Verified via the admin CLI. The real form only exposes the fields `email,password,retype,user_name`. ## 8. EXECUTION CONFIRMED (vulnerable 1.27.0) Our own PoC: `exploit/cve-2026-60004-canary.py` (pure stdlib, **no** reverse shell, the hook only writes a canary). Verification **out of band** from the host. Full chain in **0.82–0.89 s** over three runs (`evidence/poc-run-1.txt` 0.89 s, `evidence/poc-run-2.txt` 0.86 s, `evidence/poc-run-current-english.txt` 0.82 s), codes `[201, 201]` in all three. Canary (`evidence/canary-content.txt`), measured: ```text CANARY_EXEC_OK cve-2026-60004 nonce=c643d0b3-2d58-4177-837e-ab19db8b681f whoami=git uid=1000 gid=1000 self_pid=361 parent_pid=360 parent_cmdline=/usr/bin/git write-tree self_cmdline=/bin/sh hooks/post-index-change 0 0 cwd=/data/gitea/tmp/local-repo/upload.git749828539 git_dir_env=. hook_resolved=/data/gitea/tmp/local-repo/upload.git749828539/hooks/post-index-change ``` Process tree captured by the hook itself: `s6-supervise gitea` (14) → `gitea web` (16) → `git write-tree` (360) → `{post-index-chan} /bin/sh hooks/post-index-change 0 0` (361). **Contribution the advisory does NOT state:** the process that invokes the hook is **`git write-tree`** (the advisory says generically "while writing the index"), and `git_dir_env=.` operationally confirms the root↔`$GIT_DIR` equivalence. ## 9. A/B AGAINST THE PATCHED VERSION (live negative, not vacuous) Target `gitea-patched` 1.27.1, same laboratory, same configuration, same payload byte for byte. - **22 of 22 cycles: canary NOT created** (`ls: /data/canary/EXEC_CANARY.txt: No such file`). - The **second** `diffpatch` turns into answering **HTTP 500** (on the vulnerable build it answers 201). - The arrival monitor at the sink **did observe the payload on disk** on the patched build (e.g. `/data/gitea/tmp/local-repo/upload.git4155964036/hooks/post-index-change`), so the negative is not vacuous: the bytes arrive, the path is materialized, **and it does not execute**. - **Artifact added 2026-08-27** (it did not exist when this section was first written — a review round caught a claim pointing at an empty folder). The whole pair is now reproducible and shipped in `evidence/fix-verification/`: `ab-cycles-patched.log` (22/22, `codes=[201, 500]`, canary absent), `ab-monitor-patched.log` (2 captures of `hooks/post-index-change` inside `gitea-patched`), `ab-cycles-vuln.log` (**positive control**: same harness, same nonce, canary PRESENT on cycle 1 with `codes=[201, 201]`), `write-control-patched.txt` (uid 1000 can write and delete in the canary sink), and a folder-level `MANIFEST.sha256`. Payload for a fixed nonce is deterministic and identical on both arms: hook 633 B `5bc7799b…`, patch 884 B `9fe2cd1a…` (see the folder README). ## 10. Impact radius MEASURED (not asserted) Probe through the same primitive (`exploit/impact-probe.py`), result in `evidence/impact-probe-content.txt`: - The service's `app.ini` **readable in full** (1323 bytes). The **3** lines carrying secret values are `SECRET_KEY`, `INTERNAL_TOKEN`, `JWT_SECRET` (`evidence/impact-2/shell-transcript.txt`). The `DATABASE` section is **not** one of them: it declares `DB_TYPE = sqlite3`, `PATH = /data/gitea/gitea.db` and an **empty** `PASSWD`, so this lab never put a real database credential in the attacker's hands. **Correction 2026-08-27:** an earlier note here listed `DATABASE` among the secrets read. - Planted secrets **read**: `DB_PASSWORD`, `AWS_ACCESS_KEY_ID`. - **Arbitrary write confirmed** where the service lives: `ESCRITURA_OK en /data/gitea/attacker_write_test.txt` (the file was deleted afterwards). - The "client"'s repositories listable under `/data/git/repositories/…`. - Honest negative: the parent process's `environ` did **not** reveal variables with SECRET/TOKEN/PASS. - Network visible from the service context in this lab: `lo` + `172.31.224.2/24` (no egress). ## 11. Wire evidence `evidence/wire-attack.pcap` — 33,400 bytes, 72 packets, `tcp port 3000`, interface `br-403a9e21faf9`. It includes **benign control traffic** (GET `/` and an ordinary `POST contents`) so the rules' false positives can be measured. Captured with `sudo tcpdump` (without `sudo` the capture failed for `CAP_NET_RAW`: documented in §12). ## 12. DISCARDED ATTEMPTS AND BAD MEASUREMENTS (scientific record, not hidden) 1. **`grep -c diffpatch /usr/local/bin/gitea` → 0 was invalid.** That file is **581 bytes** and is a bash wrapper; the real binary is at `/app/gitea/gitea`. Measurement repeated and corrected. 2. **Imperfect date format in the canary:** `date -u +%…%6N` does not expand `%N` in busybox → the `executed_at_utc` line was left truncated (`2026-08-27T14:33:22.`). It does not invalidate anything (the proof is the nonce + the process tree) but it is documented. 3. **`total_repos=0` in the impact probe is a quoting bug** in my own hook (`\\"*.git\\"` inside a Go string). The real count of repositories is **not** established by that line; **do not use** that figure. 4. **The A/B structural probe (`ab-structural.sh`) is not reliable** and **is not used as proof**: the temporary repo lives milliseconds, `es_bare` returned `error` and the atomic checks were lost to a race. The A/B stands on §9 alone. 5. **`tcpdump` without `sudo` does not capture** on the bridge (`CAP_NET_RAW`). The first pcap did not exist; it was recaptured with sudo. Documented because that first "pcap" would have been false evidence. 6. The advisory does **not name** the real parent process, nor the `git_dir_env` value, nor the string further below. ## 13. Our own static finding, independent of the advisory In the 1.27.0 binary the format string appears: ```text conflict detected at: git ls-files -u -z: %w ``` `git ls-files -u` lists **unmerged** entries of an index: it is the collision detector of a three-way merge. Its presence in the `diffpatch` handler statically and independently confirms the add/add collision mechanism described in the advisory. ## 14. Feed-consumption context (measured in this session) KEV catalog shipped with this package (`designs/kev-record.json` / `designs/kev-feed-stats.json`): version `2026.08.27`, released `2026-08-27T17:00:36Z`, **1685** entries, **1,618,253 bytes**, `dueDate` already lapsed on **1672 (99.2 %)**, only **13** entries due within ≤30 days, **1510/1685** with the `cwes` field populated, **352** with `knownRansomwareCampaignUse=Known`. (Every figure above is a key in the shipped extract `designs/kev-feed-stats.json`: `_provenance.count`, `_provenance.catalog_bytes`, `_provenance.catalog_sha256`, `dueDate_already_lapsed`, `due_within_30_days`, `cwes_field_populated`, `ransomware_campaign_known`; `claim-gate.sh` recomputes the ones it can from that file. The numbers this section carried before the 2026-08-27 correction came from a fetch that was **not in the package**, so they are removed rather than quoted as history: a figure nobody can re-derive does not belong in a source of truth.) ## 15. Environment constraints - Fleet/parallelism numbers are operator runtime policy, not research findings: they are deliberately **not** recorded here, and the current cap lives in the operator's agent config, not in this package. - No `jq`, no `gh`. Python 3.14.6. `sudo` available. `suricata` installed. - `IEEEtran.cls` obtained from CTAN (281,957 bytes) at `paper/tex/IEEEtran.cls`. - The operator demands **strict visual auditing** of the paper and the video, plus self-improvement.