CVE-2026-60004 is a code injection flaw (CWE-94) in Gitea, the self-hosted Git forge, carrying CVSS 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). A user with write access to any repository can submit a crafted patch through the diffpatch route and have shell commands run as the operating system account that Gitea runs under. On a default installation, open self-registration supplies that write access to anyone who can reach the login page, which is why the vector reads PR:N. CISA added the CVE to the KEV catalog on August 25, 2026 with a BOD 26-04 due date of August 28, and recorded an SSVC decision of exploitation active, automatable yes, technical impact total. EPSS puts it at 0.824, in the 99.6th percentile.
The date that matters most is not on the CVE record. Gitea shipped the fix in 1.27.1 on July 27, 2026, and 1.27.2 followed on August 13. NVD published the CVE on August 26, a month after the patch and a day after the KEV listing. Any instance still exposed today has had a fix available for four weeks, and the vulnerable range reaches back to 1.17, so this is not a regression that landed last week. It is a long-lived bug in a feature most operators have never knowingly used.
Two things follow from that for triage. First, version inventory is the whole exercise here: there is no vendor workaround, and the affected range spans nine minor releases. Second, the reported in-the-wild activity is narrower than the KEV entry implies, and the write-up below is explicit about where the evidence stops.
Am I vulnerable?
Affected versions
| Product |
Vulnerable range |
Fixed in |
| Gitea |
1.17.0 through 1.27.0 inclusive |
1.27.1 (July 27, 2026); 1.27.2 (August 13, 2026) is the current release and the better target |
Four preconditions must all hold, per the vendor advisory: Git 2.32 or newer on the server (the three-way merge fallback the bug relies on), the diffpatch route enabled, a temporary filesystem that is both writable and executable, and an account with repository write access. Git 2.32 shipped in 2021, so treat that one as met.
Diagnostic commands
All read-only.
Remote version banner, no credentials, using an endpoint confirmed present in the v1.27.1 Swagger spec:
curl -4 -s https://gitea.example.com/api/v1/version
A response of {"version":"1.24.3"} or anything else from 1.17.0 up to and including 1.27.0 is vulnerable. Build suffixes such as 1.27.2+dev-... are normal. If this 404s or demands authentication, the instance may have REQUIRE_SIGNIN_VIEW set or sit behind a proxy, so fall back to a local check.
Local binary: gitea --version. This reports the binary on PATH, which is not always the one systemd starts. Check the exact path in your unit's ExecStart.
Container: docker exec <gitea-container> gitea --version, cross-checked against the running image with docker inspect --format '{{.Config.Image}}' <gitea-container>.
Distribution packages: dpkg -l | grep -i gitea or rpm -qa | grep -i gitea. Distro packages lag upstream more often than self-managed binaries do.
Is the pre-auth path open? Adjust the path for your install (/etc/gitea/app.ini, or /data/gitea/conf/app.ini in containers):
grep -nEi 'DISABLE_REGISTRATION|REQUIRE_SIGNIN_VIEW|REGISTER_EMAIL_CONFIRM|ENABLE_CAPTCHA|ENABLE_OPENID_SIGNUP' /etc/gitea/app.ini
Absent keys take the Gitea default, so no output is a finding rather than an all-clear.
"Vulnerable" here means any instance in the affected range that has users with repository write access. Closing registration narrows who can reach the flaw; it does not remove it. An internal instance with fifty trusted developers is still a privilege-escalation-to-host problem.
Vulnerability
Gitea's diffpatch feature lets a user apply a patch to a repository from the web UI or the API. To do that, the server made a temporary clone and applied the patch there. The advisory's root cause is that this temporary clone was bare. In a bare repository the clone root is $GIT_DIR itself, which means the hooks/ directory sits directly under the path the patch operates on.
From there the mechanism is a collision. Applying the same patch twice produces an add/add conflict, and git apply's three-way fallback (available since Git 2.32) resolves it by checking out the indexed path even when the operation was requested against the index only. An executable file placed at hooks/post-index-change therefore becomes a live Git hook, and Git runs that hook itself when it next writes the index. The commands in it execute as the Gitea service account, typically git or gitea.
post-index-change is the load-bearing detail for defenders. It is a standard Git hook, documented in githooks, and Gitea never creates it. Gitea generates only pre-receive, update, post-receive and proc-receive, each as a wrapper that delegates to the Gitea binary. Its generated hook files all carry the literal marker # AUTO GENERATED BY GITEA, DO NOT MODIFY. An executable file in a Gitea-managed hooks/ directory that lacks that marker is either an administrator's custom hook or a plant, and the set of such files in a healthy instance is small enough to enumerate.
The fix in 1.27.1 is a one-line change in services/repository/files/patch.go: the temporary clone became non-bare, so the clone root is no longer $GIT_DIR and a patched-in path can no longer land in a live hooks/ directory. Shai Rod (NightRang3r) of Salesforce reported the flaw.
NVD classifies it as CWE-94. The S:U in the vector is worth reading against the impact: CVSS scores scope as unchanged because the code executes under the same authority Gitea already holds, but that authority includes every hosted repository on disk and the credentials in the application's configuration.
Threat model
Who would exploit this: The most likely operators are opportunistic criminal crews running mass internet scans, because on a default install the flaw needs no existing credentials: self-registration supplies the write access diffpatch requires. The one documented compromise fits that profile. Initial-access brokers and supply-chain-focused actors are a credible second tier, since a Git forge holds source code, CI credentials, deploy keys, signing material and container images. State-aligned interest is plausible for the same reasons but appears in no published reporting.
What they are after, ranked by what the evidence actually supports:
- Cryptomining for direct revenue. The only goal confirmed in the wild.
- A foothold on the host as the Gitea service account.
- Credentials and secrets. The advisory states the flaw may expose application secrets, database credentials and OAuth tokens depending on deployment.
- Lateral movement into build infrastructure via harvested CI runner tokens, deploy keys and webhook secrets.
- Source code and Git history exfiltration, including credentials committed years ago and never rotated.
- Supply-chain implantation into downstream builds. The flaw enables it and it is the highest-consequence outcome, but no public reporting shows it happening.
Attack chain: The attacker reaches the web application over HTTP and, where self-registration is open, registers and creates a repository to obtain write access. Crafted patch content submitted to the diffpatch route lands an executable file in the temporary clone's hook directory, and a subsequent index operation causes Git to run it. In the observed incident, that execution pulled down a next-stage miner payload.
Blast radius: Command execution as the Gitea service account typically means read access to the configuration file, the process environment, every hosted repository on disk, and the backing database. The reach then extends to whatever the forge brokers: CI runner tokens, SSH deploy keys pointed at production, webhook secrets, container-registry credentials and repository-level cloud secrets. Those are valid credentials rather than exploits, so adjacent systems accept them, and self-hosted forges tend to sit on internal networks that build infrastructure already trusts.
On the evidence: all public technical incident detail traces to one self-hosted operator's write-up, relayed by The Hacker News and Help Net Security. Their hosting provider flagged sustained CPU use as a terms violation. The instance had registration open and sign-in view off; registration to code execution took roughly eleven seconds. The dropper cleared LD_PRELOAD and LD_LIBRARY_PATH, killed competing high-CPU processes, fetched an architecture-matched payload, ran it, and deleted the downloads, leaving no cron job, systemd unit or SSH key. It did not survive a container restart, which matters directly for responders: triage the running host before you bounce it, because restarting erases the evidence. SecurityWeek notes no public exploitation reports predate the KEV listing and that the actor and objectives remain unclear. No security vendor has published first-party telemetry, and CISA does not publish the data behind a KEV addition. Shadowserver's roughly 5,000 exposed Gitea instances is a population figure, not a count of vulnerable hosts.
Mitigation
Patch
Upgrade to 1.27.2 or later. 1.27.1 contains the security fix, but 1.27.2 shipped on August 13 with subsequent bugfixes and is the better target.
services:
gitea:
image: gitea/gitea:1.27.2 # was <= 1.27.0 - anything 1.17..1.27.0 is vulnerable
restart: always
volumes:
- ./gitea:/data
# docker compose pull gitea && docker compose up -d gitea
# docker compose exec gitea gitea --version # confirm >= 1.27.1
The upstream change, for anyone validating a backport:
// before: bare temp clone, so the clone root WAS $GIT_DIR and hooks/ was live
// - if err := t.Clone(ctx, opts.OldBranch, true); err != nil {
// after:
// 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 {
return nil, err
}
Configuration mitigation
Gitea publishes no configuration workaround. The advisory's only remedy is the upgrade. The settings below close the unauthenticated path and are worth setting on their own merits, but they do not fix the bug and do nothing about users who already have write access.
Close anonymous account creation in app.ini:
[service]
; stop anonymous account creation - this is what turns the bug pre-auth on default installs
DISABLE_REGISTRATION = true
; if you must keep registration open, at least gate it
REGISTER_EMAIL_CONFIRM = true
ENABLE_CAPTCHA = true
; do not let anonymous users browse the instance at all
REQUIRE_SIGNIN_VIEW = true
[openid]
; OpenID signup bypasses DISABLE_REGISTRATION intent if left on
ENABLE_OPENID_SIGNUP = false
Confirm the admin custom-hooks UI is off (defence in depth, not a fix for this CVE, which writes hooks on disk directly):
[security]
; Gitea default is true. Setting it does not remove hooks already present in repos.
DISABLE_GIT_HOOKS = true
Compensating controls
None of these substitutes for 1.27.2.
- Mount Gitea's temporary filesystem
noexec. A writable and executable temp filesystem is one of the four preconditions the advisory lists, so removing the execute bit removes a required condition rather than merely raising difficulty.
- Block the diffpatch routes at the reverse proxy or WAF until patched: the web route
POST /{owner}/{repo}/_diffpatch/{branch} and the API route POST /api/v1/repos/{owner}/{repo}/diffpatch. Most organisations never use this feature, so the availability cost is usually nil.
- Take a hook inventory now, before patching, across your repository storage. It is the cheapest way to make a later compromise assessment answerable, and the healthy baseline is small.
- Constrain egress from the Gitea host. A forge talks to a predictable set of destinations, which makes outbound allowlisting practical and turns a payload fetch into a visible event.
- Reduce what the service account reaches. Scope database credentials to the minimum the application needs, and keep repository-level secrets out of the instance where a separate secret store will do.
- Treat credential rotation as incident response, not patching. If an instance was in the affected range and reachable by untrusted users, then database credentials, OAuth tokens, CI/CD secrets, deploy keys and webhook secrets on it are in scope.
Detection (starter rules, validate before deploying)
These rules are AI-generated starter content. Test against your own telemetry and tune falsepositives before deploying to production. Both are post-exploitation detections. Neither has been validated against real exploitation.
One limitation belongs up front. Public proof-of-concept code retrieves command output by writing it into Git objects and reading it back over authenticated smart HTTP, which means successful exploitation can produce no outbound network traffic at all. Absence of egress does not rule out compromise, so pair the host rules below with the access-log signals.
Network and log signals
- Any POST to the diffpatch route in
access.log (default %(GITEA_WORK_DIR)/log/access.log, NCSA common format including the authenticated identity). Both POST /{owner}/{repo}/_diffpatch/{branch} and POST /api/v1/repos/{owner}/{repo}/diffpatch reach the vulnerable code. Most instances never use the feature, so alert on all of it and baseline down rather than trying to fingerprint payloads.
- Two or more POSTs to the same diffpatch path from one identity within seconds, with identical or near-identical
Content-Length. The collision the bug depends on requires the same patch to be submitted twice, so a repeated identical submission is what distinguishes exploitation from a legitimate single patch apply. This is the highest-value signal in the list.
- The registration chain:
POST /user/sign_up, then POST /repo/create, then a diffpatch POST, all within minutes from one identity or source IP. An account whose entire history is those three actions is high signal.
- Repositories that receive diffpatch traffic but no clone, fetch or push. Legitimate repositories get pushed to. A repository that exists only to carry a patch does not. Watch also for an unexpected extra branch appearing in such a repository.
- Creation of
hooks/post-index-change anywhere under repository storage. Default roots: /var/lib/gitea/data/gitea-repositories/<owner>/<repo>.git/hooks/ for binary and Debian installs, /data/git/repositories/ in the official Docker image. Confirm yours from [repository] ROOT in app.ini rather than assuming. Any executable hook other than pre-receive, update, post-receive or proc-receive warrants investigation.
- Executable files in a
hooks/ directory that lack the # AUTO GENERATED BY GITEA, DO NOT MODIFY marker.
- New executable files, or execute-bit changes, in Gitea's temp area (
APP_DATA_PATH/tmp, commonly /var/lib/gitea/data/tmp, or the process TMPDIR).
- Shells and downloaders under the service account:
sh, bash, python, curl, wget, nc, socat with the gitea binary or a git process as an ancestor. Gitea does not shell out for normal repository work; it execs git directly. The legitimate exception is its own delegated hooks, which invoke gitea hook --config=<path> <hookname>.
- Outbound connections from the
git or gitea UID to stratum mining ports (3333, 4444, 5555, 7777, 14444, 45700) or unfamiliar hosting providers, paired with CPU anomalies on a service that is normally I/O bound.
- Router WARN entries for failed or slow diffpatch routes and
git apply errors in %(GITEA_WORK_DIR)/log/gitea.log. Failed attempts leave traces here even when the access log shows only a non-200.
YARA
rule Gitea_CVE_2026_60004_Suspicious_Git_Hook_Script
{
meta:
description = "Starter rule: flags a Git hook script under Gitea repository storage that fetches and executes remote content, stages a payload in a world-writable directory, or carries coin-miner configuration. CVE-2026-60004 lets a user with repository write access plant an executable hooks/post-index-change script via the diffpatch route; git then runs it as the Gitea OS user."
author = "CVE Brief (AI-generated starter rule, validate before deploying)"
date = "2026-08-28"
cve = "CVE-2026-60004"
reference = "https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m"
reference_githooks = "https://git-scm.com/docs/githooks"
scan_scope = "Run recursively over Gitea repository storage, e.g. yara -r gitea.yar /var/lib/gitea/data/gitea-repositories (binary install) or /data/git/repositories (official Docker image)"
confidence = "medium"
strings:
$sb_sh = "#!/bin/sh"
$sb_bash = "#!/bin/bash"
$sb_env = "#!/usr/bin/env"
$sb_py = "#!/usr/bin/python"
$sb_perl = "#!/usr/bin/perl"
$gitea_generated = "# AUTO GENERATED BY GITEA, DO NOT MODIFY" ascii
$dl1 = "curl " ascii
$dl2 = "wget " ascii
$dl3 = "/dev/tcp/" ascii
$dl4 = "urllib.request" ascii
$dl5 = "http://" ascii
$dl6 = "https://" ascii
$ex1 = "| sh" ascii
$ex2 = "|sh" ascii
$ex3 = "| bash" ascii
$ex4 = "|bash" ascii
$ex5 = "chmod +x" ascii
$ex6 = "chmod 755" ascii
$ex7 = "chmod 0755" ascii
$ex8 = "base64 -d" ascii
$ex9 = "base64 --decode" ascii
$ex10 = "nohup " ascii
$ex11 = "setsid " ascii
$ex12 = "eval(" ascii
$st1 = "/tmp/" ascii
$st2 = "/var/tmp/" ascii
$st3 = "/dev/shm/" ascii
$mine1 = "stratum+tcp://" ascii nocase
$mine2 = "stratum+ssl://" ascii nocase
$mine3 = "xmrig" ascii nocase
$mine4 = "--donate-level" ascii nocase
$mine5 = "cryptonight" ascii nocase
$mine6 = "randomx" ascii nocase
condition:
filesize < 100KB
and ( for any of ($sb*) : ( $ at 0 ) )
and not $gitea_generated
and (
2 of ($mine*)
or ( any of ($dl*) and any of ($ex*) )
or ( any of ($st*) and 2 of ($ex*) )
)
}
Sigma
title: Gitea Service Process Spawning Shell, Downloader or Miner (CVE-2026-60004)
id: 7092c2e3-331b-4bb7-b1fe-95adb796ac7c
status: experimental
description: |
Detects a shell, script interpreter, network downloader or coin-miner process spawned
beneath the Gitea service process (or beneath the git child processes Gitea invokes)
while running as the git/gitea service account. CVE-2026-60004 lets a user with
repository write access submit a crafted patch to Gitea's diffpatch route
(web POST /{owner}/{repo}/_diffpatch/{branch} or API POST /api/v1/repos/{owner}/{repo}/diffpatch)
that lands an executable hooks/post-index-change script inside the bare temporary clone;
git then executes that hook as the Gitea OS user while writing the index. Default open
self-registration means the attacker can create the account and the repository themselves.
Observed post-exploitation includes cryptominer deployment.
Note - the service_account selection requires the collector to populate the User field
(auditd auid/uid, Sysmon-for-Linux User). Drop that selection from the condition if your
pipeline does not normalise it, at the cost of precision.
references:
- https://github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84m
- https://nvd.nist.gov/vuln/detail/CVE-2026-60004
- https://git-scm.com/docs/githooks
author: CVE Brief (AI-generated starter rule, validate before deploying)
date: 2026/08/28
tags:
- attack.execution
- attack.t1059.004
- attack.persistence
- attack.t1505
- attack.impact
- attack.t1496
logsource:
product: linux
category: process_creation
detection:
parent_gitea_image:
ParentImage|endswith:
- '/gitea'
- '/git'
parent_gitea_cmdline:
ParentCommandLine|contains:
- 'gitea web'
- 'gitea serv'
- 'gitea hook'
service_account:
User:
- 'git'
- 'gitea'
child_downloader:
Image|endswith:
- '/curl'
- '/wget'
- '/nc'
- '/ncat'
- '/netcat'
- '/socat'
child_interpreter:
Image|endswith:
- '/sh'
- '/bash'
- '/dash'
- '/zsh'
- '/ksh'
- '/busybox'
- '/python'
- '/python3'
- '/perl'
- '/ruby'
- '/php'
suspicious_cmdline:
CommandLine|contains:
- 'post-index-change'
- '/tmp/'
- '/var/tmp/'
- '/dev/shm/'
- '/dev/tcp/'
- 'base64 -d'
- 'base64 --decode'
- 'chmod +x'
- 'nohup '
- 'setsid '
- 'curl '
- 'wget '
- 'stratum+tcp'
- 'stratum+ssl'
- 'xmrig'
filter_gitea_own_hooks:
CommandLine|contains:
- 'hooks/pre-receive.d/gitea'
- 'hooks/post-receive.d/gitea'
- 'hooks/update.d/gitea'
- 'hooks/proc-receive.d/gitea'
- ' hook --config='
condition: (parent_gitea_image or parent_gitea_cmdline) and service_account and (child_downloader or (child_interpreter and suspicious_cmdline)) and not filter_gitea_own_hooks
fields:
- Image
- CommandLine
- ParentImage
- ParentCommandLine
- User
- CurrentDirectory
falsepositives:
- Gitea legitimately writes and executes its own delegated hooks (hooks/<name>.d/gitea); those are excluded by filter_gitea_own_hooks, but a site that has renamed the Gitea binary or hook path will still alert.
- Administrator-authored custom server-side hooks placed in hooks/<name>.d/ frequently call curl, wget or a shell to notify chat, CI or ticketing systems.
- CI/CD runners, Gitea Actions runners, backup and mirror jobs, and git-lfs helpers running under the same git/gitea service account.
- Legitimate administration performed with sudo -u git, or a maintenance shell opened on the Gitea host.
level: high
Rule notes
The Sigma rule anchors on a parent/child relationship Gitea does not normally produce, since it execs git directly rather than shelling out, and then filters Gitea's own delegated hooks by their gitea hook --config= invocation. The YARA rule keys on a hook script that both retrieves and executes remote content while lacking Gitea's generated-file marker. Known limitations: both are post-exploitation detections and will miss the quiet variant that stores command output in Git objects and makes no outbound connection, which is why the repeated-identical-POST access-log signal matters more than either rule; the YARA rule assumes a text script and will not match a compiled ELF or heavily obfuscated payload; and administrator-written custom hooks that call curl are a genuine false-positive source that has to be baselined per environment.
References