# Remediation — CVE-2026-60004 (Gitea `diffpatch` → RCE) **Who this is for:** anyone who administers Gitea. Dates and figures measured in `designs/FACTS.md`. **We do not claim discovery:** the vendor published the advisory and the fix; the advisory also includes a complete working PoC. ## 1. The right action, in order 1. **Upgrade to Gitea ≥ 1.27.1.** It is the only remedy that closes the defect. Real fix: commit `d7bc52beeadff4be5f5690de4d5de42abd10affe` (2026-07-26), which changes the temporary *clone* from **bare** to **non-bare** in `services/repository/files/patch.go`. 2. If you cannot upgrade today: **disable the `diffpatch` route** and **close open registration** (both reduce prerequisites, **neither** removes the defect for someone who already has write access). 3. **Assume compromise if registration was ever open**, not just "patch and watch". Reasoning in §3. ## 2. Verifying that you are patched ```bash curl -s http:///api/v1/version # -> {"version":"1.27.1"} or higher ``` The version API confirms the branch that is running. **It is not proof that the `diffpatch` route is out of service** — check that separately if you decide to mitigate instead of upgrade. ## 3. Why this is "assume compromise", not "patch" Measured in the laboratory (§10 of FACTS.md), using the command-execution primitive: - The service's `app.ini` readable in full (1,323 bytes): **3 lines carrying secret values**, `SECRET_KEY`, `INTERNAL_TOKEN` and `JWT_SECRET` (`evidence/impact-2/shell-transcript.txt`; values truncated or length-reported in the artifact). - Credentials planted on the service's disk: **read**. - **Arbitrary write confirmed** in `/data/gitea`, the directory where the service lives. - Repositories under `/data/git/repositories/…` listable. Read of `app.ini` plus write into `/data/gitea` means persistence and data access follow directly from the primitive: we read the config (1,323 bytes, 3 secret-bearing lines) and read planted credentials back through the same hook. **We did not exercise IP exfiltration in this lab.** The execution runs as the service's OS user; we observed that the `DATABASE` section of `app.ini` was readable, but **we never held a database credential from it**: in our lab that section declared `DB_TYPE = sqlite3`, `PATH = /data/gitea/gitea.db` and an **empty** `PASSWD`, so there was nothing to steal there. The database credential we did read (`DB_PASSWORD`) was one **we planted ourselves** as a fictional file. We did not connect to any database. In a real deployment that section typically holds live credentials, and integration/OAuth secrets often sit in the same file — that is an inference about other people's configurations, which is why we recommend rotating it and do not report it as measured here. **And the prerequisite is cheap:** a **non-admin** account is enough, and with open registration an unauthenticated visitor creates one themselves. Verified: `POST /user/sign_up` → HTTP 303 → account with `IsActive true`, `IsAdmin false`, and from that account the full RCE chain worked in **0.82-0.89 s**. **And it is silent:** the hook's return value does **not** propagate into the `diffpatch` response. Both of the exploit's requests answered **HTTP 201**. A log that records only status codes will see two successful writes. ## 4. Indicators of compromise (see playbook-ir.md for the procedure) - `hooks/*` files inside `/data/gitea/tmp/local-repo/upload.git*/` (the `diffpatch` temporary clone). On a clean instance there is nothing there: user repositories live in `/data/git/repositories/`. - Process lineage `gitea web` → `git write-tree` → `/bin/sh hooks/…`. Measured: that is the real parent, not an HTTP shell. - Two `POST …/diffpatch` requests with an **identical** body back to back, both 201. - New branches or commits that do not correspond to human activity, with a service author. - Changes to `app.ini` or new files under `/data/gitea/` that nobody put there. ## 5. What this does NOT fix The fix does **not** add any path blacklist and does not change the `git apply` flags. It only changes the structural condition (worktree separated from `$GIT_DIR`). Practical consequence for the defender: **a rule that only looks at the patch content is fragile** — the same payload byte for byte is inert after the fix, and — this is an inference from the diff, we did not build that variant — a patch that never mentions `hooks/` but writes into another sensitive path would likely look like a "normal" patch to the parser. Detect **behavior**, not only the signature. ## 6. Compliance context - CISA KEV: added **2026-08-25** (`dateAdded` verbatim in `designs/kev-record.json`), federal due date **2026-08-28**. - The KEV note itself links **BOD 26-04** (risk-based prioritization, 3/14/60-day windows). - NVD CVSS (as published): `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H` = **9.8 CRITICAL**, CWE-94. NVD tags this score as **Secondary** (not the Primary score of the record) — worth knowing before you quote it in a ticket. - `knownRansomwareCampaignUse = Unknown` in KEV: do not claim ransomware use.