Leaving the keys inside a photocopier that prints its own lock picks
CVE-2026-89094 is a remote code execution flaw in Forgejo's repository-from-template feature. When a user creates a new repository from a template, Forgejo clones the template, deletes .git, expands variables listed in .forgejo/template, then runs git init. A crafted template can abuse variable expansion to recreate a .git/hooks/ directory containing executable scripts. When git init fires, it adopts the pre-existing .git folder and runs those hooks as the Forgejo service account — giving the attacker arbitrary OS-level command execution. Affected versions: Forgejo 16.0.0–16.0.3, all versions prior to 15.0.8 (LTS branch), and Gitea ≤ 1.25.4 (shared codebase).
The vendor rates this CVSS 9.9 CRITICAL with Scope Changed (S:C), meaning the compromise escapes the Forgejo application boundary into the underlying OS. That rating is accurate and, if anything, generous only in not being a flat 10.0. The PR:L (low privilege) prerequisite is nearly zero friction on a code forge: any registered user can own a repo, mark it as a template, create from it, and pop the box. Forgejo *is* the supply chain — it holds every repo, every CI secret, every deploy key. A compromised Forgejo host gives an attacker silent code injection across every project it hosts.
5 steps from start to impact.
Obtain any authenticated account
- Network access to the Forgejo web UI (typically TCP 3000 or behind a reverse proxy on 443)
- A valid user session — either via open registration or an existing account
- Forgejo ≥ v9 defaults self-registration to disabled on initial install
- Instances behind VPN or internal-only networking limit external attackers
Create a malicious template repository
.forgejo/template (or .gitea/template for Gitea) listing files that contain variable-expansion payloads designed to write a .git/hooks/post-checkout or pre-commit script during expansion. Any user who owns a repo can toggle the template flag — no elevated permissions required.- Repository creation permission (granted to all authenticated users by default)
- Ability to push files to the repo (owner always has this)
- None — this is a standard user workflow
Trigger repo-from-template creation
.git, then runs variable expansion on every file listed in .forgejo/template. The crafted payload writes a new .git/hooks/ directory with executable scripts. No special timing or race condition is needed — the expansion is deterministic and synchronous.- Target instance has not yet patched to 16.0.4 / 15.0.8 / Gitea 1.25.5
- None once steps 1–2 are complete — single HTTP request
repo.create_from_template event; correlate with unusual .git/hooks/ file writes in the repo storage pathgit init adopts malicious hooks → RCE
git init on the expanded working directory. Because the crafted .git/ folder already exists, Git adopts it instead of creating a fresh one. The hook scripts execute immediately with the privileges of the Forgejo process (typically a dedicated forgejo or git service account). The attacker now has arbitrary command execution on the host OS.- Forgejo service account has OS-level execution privileges (always true)
- None — this is the normal code path
.git/hooks/ containing #!/bin/ patterns in template-derived pathsPost-exploitation: source-code and secret exfiltration
app.ini, dump CI/CD runner tokens, deploy keys, and OAuth secrets. They can silently modify source code in any repository to inject backdoors into downstream builds — a textbook supply-chain attack. If Forgejo Actions runners are configured, the attacker can pivot to runner hosts.- Forgejo service account has read access to the data directory and config (always true by design)
- Encrypted secrets-at-rest (not default in Forgejo)
- Network segmentation between Forgejo host and CI runners
app.ini and the repositories directory; anomalous outbound connections from the Forgejo host; git audit logs showing unexpected ref updatesThe supporting signals.
| In-the-Wild Exploitation | No confirmed active exploitation as of 2026-09-12. Not listed in CISA KEV. |
|---|---|
| Proof-of-Concept | No public PoC as of disclosure. However, the attack is mechanically simple — a skilled attacker can reproduce it from the advisory description alone within hours. |
| EPSS | 0.5% 30-day exploitation probability — low, but expected to rise quickly once a PoC surfaces given the trivial attack complexity. |
| KEV Status | Not listed. CISA KEV entry unlikely unless active exploitation is confirmed. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H — Network-accessible, low complexity, low privilege, no user interaction, scope changed. The vector is textbook worst-case for an authenticated RCE. |
| Affected Versions | Forgejo 16.0.0–16.0.3, Forgejo ≤ 15.0.7 (LTS), Gitea ≤ 1.25.4 (shared codebase) |
| Fixed Versions | Forgejo 16.0.4, Forgejo 15.0.8 (LTS), Gitea 1.25.5 — all released 2026-09-10 |
| Exposure Data | Forgejo and Gitea combined have tens of thousands of internet-facing instances. Gitea ships with open registration enabled by default, expanding the unauthenticated-to-authenticated pipeline. Exact scanner counts unavailable at time of assessment. |
| Disclosure Date | 2026-09-10 — coordinated disclosure with patch release |
| Reporter | Reported to the Forgejo security team; specific researcher name not publicly credited in the advisory as of this assessment. |
noisgate verdict.
The single most decisive factor is the role multiplier: Forgejo/Gitea is canonically a source-code forge and CI/CD supply-chain component — compromising the host means silent code injection across every hosted project, secret exfiltration, and runner pivot, making the blast radius supply-chain-scale by definition. The authentication prerequisite (PR:L) provides negligible friction because any registered user — including self-registered accounts on Gitea's default-open instances — can execute the full chain with zero user interaction.
Why this verdict
- Trivial attack chain: A single authenticated user can go from account creation to full RCE in three clicks — create repo, mark as template, create from template. No race condition, no memory corruption, no brute force.
- Role multiplier — supply-chain-scale blast radius: Forgejo/Gitea IS the code forge. ≥90% of Forgejo installations exist specifically to host source code and CI/CD pipelines. Compromising the Forgejo host grants read/write access to every repository, every deploy key, every CI runner token. The chain ends in supply-chain compromise, meeting the CRITICAL floor.
- Scope Changed (S:C) is real: Execution escapes the Forgejo application into the host OS as the service account. From there, lateral movement to CI runners, databases, and artifact stores is a standard post-exploitation playbook.
- Gitea shares the vulnerable codebase: The blast radius extends beyond Forgejo to the much larger Gitea installed base (Gitea ≤ 1.25.4), multiplying the population of vulnerable instances significantly.
- Minimal friction on default configs: Gitea ships with open registration enabled. Many legacy Forgejo instances predate the default-off change. The PR:L barrier is effectively PR:N for a meaningful fraction of the installed base.
Why not higher?
A 10.0 would require PR:N (no authentication at all). The CVSS vector correctly reflects PR:L, and on instances with self-registration disabled and behind VPN, the attacker does need a valid credential or internal network position. The 9.9 accurately captures this single friction point.
Why not lower?
Downgrading below CRITICAL would require that the target component is NOT canonically a high-value supply-chain asset, or that the chain has significant real-world friction. Neither applies here. Forgejo is, by definition, the source-code store — the supply chain itself. The chain has near-zero friction for any authenticated user. Even on locked-down instances, any insider or compromised credential holder can exploit this in seconds. The CRITICAL floor set by the role multiplier cannot be broken without evidence that <1% of Forgejo instances hold meaningful source code, which is counterfactual.
What to do — in priority order.
- Patch to Forgejo 16.0.4 / 15.0.8 / Gitea 1.25.5 immediately — The patch removes any recreated
.gitdirectory after template variable expansion and beforegit init. This is the definitive fix. Under the noisgate mitigation SLA for CRITICAL, deploy within 3 days (by 2026-09-13 given disclosure on 09-10). - Disable repository-from-template creation — If immediate patching is blocked, disable the template-repository feature at the application level or via reverse-proxy rules blocking the
/repo/createendpoint withtemplateparameters. This eliminates the attack surface entirely until the patch is applied. - Restrict repository creation to admin-approved users — Set
DISABLE_REGISTRATION = trueand limit repo-creation permissions to trusted teams. This raises the PR:L bar from 'anyone on the internet' to 'vetted internal users', reducing (but not eliminating) risk. - Monitor Forgejo process for anomalous child processes — Deploy an EDR rule or auditd filter alerting on the Forgejo service account spawning unexpected shell processes, particularly from paths containing
/hooks/. This provides detection-in-depth while patching is underway. - Audit existing repos created from templates — Search the Forgejo data directory for repositories containing
.git/hooks/scripts with executable content that was not placed there by Forgejo itself. Any findings indicate possible prior exploitation.
- WAF / reverse-proxy request filtering — the malicious payload is in the template repository's file content (pushed via standard Git protocol), not in HTTP request parameters. A WAF cannot inspect or block it.
- Network segmentation alone — if the attacker already has an account (insider threat, compromised creds), network controls do not prevent exploitation. The attack uses the normal Forgejo web UI.
- Git server-side hooks — Forgejo's own hook execution is the attack vector; adding more hooks does not mitigate the template-expansion flaw.
Crowdsourced verification payload.
Run this script on the Forgejo/Gitea host as any user with read access to the Forgejo binary. Example: bash check_cve_2026_89094.sh /usr/local/bin/forgejo or bash check_cve_2026_89094.sh /usr/local/bin/gitea. No elevated privileges required beyond reading the binary version.
#!/usr/bin/env bash
# check_cve_2026_89094.sh — Detect CVE-2026-89094 (Forgejo/Gitea template RCE)
# Usage: bash check_cve_2026_89094.sh /path/to/forgejo|gitea
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -euo pipefail
BINARY="${1:-}"
if [[ -z "$BINARY" ]]; then
echo "Usage: $0 /path/to/forgejo|gitea"
exit 2
fi
if [[ ! -x "$BINARY" ]]; then
echo "UNKNOWN — binary not found or not executable: $BINARY"
exit 2
fi
VERSION_OUTPUT=$( "$BINARY" --version 2>/dev/null || true )
# Expect output like: "Forgejo version 16.0.4 built with ..." or "Gitea version 1.25.5 ..."
if echo "$VERSION_OUTPUT" | grep -qi 'forgejo'; then
PRODUCT="forgejo"
VER=$(echo "$VERSION_OUTPUT" | grep -oP '\d+\.\d+\.\d+' | head -1)
elif echo "$VERSION_OUTPUT" | grep -qi 'gitea'; then
PRODUCT="gitea"
VER=$(echo "$VERSION_OUTPUT" | grep -oP '\d+\.\d+\.\d+' | head -1)
else
echo "UNKNOWN — could not determine product from version output: $VERSION_OUTPUT"
exit 2
fi
if [[ -z "$VER" ]]; then
echo "UNKNOWN — could not parse version number from: $VERSION_OUTPUT"
exit 2
fi
IFS='.' read -r MAJOR MINOR PATCH <<< "$VER"
if [[ "$PRODUCT" == "forgejo" ]]; then
# Patched: 16.0.4+ or 15.0.8+
if (( MAJOR > 16 )); then
echo "PATCHED — $PRODUCT $VER"
exit 0
elif (( MAJOR == 16 && MINOR == 0 && PATCH >= 4 )); then
echo "PATCHED — $PRODUCT $VER (>= 16.0.4)"
exit 0
elif (( MAJOR == 16 && MINOR > 0 )); then
echo "PATCHED — $PRODUCT $VER (16.x > 16.0)"
exit 0
elif (( MAJOR == 15 && MINOR == 0 && PATCH >= 8 )); then
echo "PATCHED — $PRODUCT $VER (>= 15.0.8 LTS)"
exit 0
elif (( MAJOR == 15 && MINOR > 0 )); then
echo "PATCHED — $PRODUCT $VER (15.x > 15.0)"
exit 0
else
echo "VULNERABLE — $PRODUCT $VER (needs 16.0.4+ or 15.0.8+)"
exit 1
fi
elif [[ "$PRODUCT" == "gitea" ]]; then
# Patched: 1.25.5+
if (( MAJOR > 1 )); then
echo "PATCHED — $PRODUCT $VER"
exit 0
elif (( MAJOR == 1 && MINOR > 25 )); then
echo "PATCHED — $PRODUCT $VER"
exit 0
elif (( MAJOR == 1 && MINOR == 25 && PATCH >= 5 )); then
echo "PATCHED — $PRODUCT $VER (>= 1.25.5)"
exit 0
else
echo "VULNERABLE — $PRODUCT $VER (needs 1.25.5+)"
exit 1
fi
fi
echo "UNKNOWN — unhandled version: $PRODUCT $VER"
exit 2If you remember one thing.
.git/hooks/ content that might indicate prior exploitation. If you also run Gitea — same bug, same urgency.Sources
- Forgejo Release Notes — v16.0.4 and v15.0.8
- SecurityOnline — CVE-2026-89094 Forgejo RCE Analysis
- ByteIota — Forgejo CVE-2026-89094 CVSS 9.9 RCE
- LWN.net — Forgejo 16.0.4 and 15.0.8 Security Advisory
- THREATINT — CVE-2026-89094
- VulDB — CVE-2026-89094
- Forgejo Hardening Guide — Disable Self-Registration
- Lobsters Discussion — Forgejo 16.0.4 Critical Fix
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.