← Back to Feed CACHED · 2026-09-12 08:29:10 · CACHE_KEY CVE-2026-89094
CVE-2026-89094 · CWE-1336 · Disclosed 2026-09-10

Forgejo before 16.0.4

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
Do you agree?
01 · The Real Story

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.

"Any authenticated user turns a template repo into full RCE on your code forge — supply-chain game over."
02 · The Attack Path

5 steps from start to impact.

STEP 01

Obtain any authenticated account

The attacker registers or already holds any user-level account on the target Forgejo/Gitea instance. Newer Forgejo installs default to self-registration disabled, but Gitea still ships open-registration-on by default, and many legacy Forgejo instances never flipped the switch. On instances with open registration, this step is free; on closed instances it requires a compromised or legitimately provisioned credential.
Conditions required:
  • 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
Where this breaks in practice:
  • Forgejo ≥ v9 defaults self-registration to disabled on initial install
  • Instances behind VPN or internal-only networking limit external attackers
STEP 02

Create a malicious template repository

The attacker creates a new repository and marks it as a *template*. Inside the repo they craft .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.
Conditions required:
  • Repository creation permission (granted to all authenticated users by default)
  • Ability to push files to the repo (owner always has this)
Where this breaks in practice:
  • None — this is a standard user workflow
STEP 03

Trigger repo-from-template creation

The attacker (or any other user) clicks New Repository → Use This Template. Forgejo clones the template, deletes .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.
Conditions required:
  • Target instance has not yet patched to 16.0.4 / 15.0.8 / Gitea 1.25.5
Where this breaks in practice:
  • None once steps 1–2 are complete — single HTTP request
Detection/coverage: Audit logs will show a repo.create_from_template event; correlate with unusual .git/hooks/ file writes in the repo storage path
STEP 04

git init adopts malicious hooks → RCE

Forgejo calls 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.
Conditions required:
  • Forgejo service account has OS-level execution privileges (always true)
Where this breaks in practice:
  • None — this is the normal code path
Detection/coverage: EDR/auditd should flag child processes spawned by the Forgejo binary executing shell scripts from a repo hooks directory; YARA rules on .git/hooks/ containing #!/bin/ patterns in template-derived paths
STEP 05

Post-exploitation: source-code and secret exfiltration

With OS access as the Forgejo service account, the attacker can read every repository on disk, extract database credentials from 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.
Conditions required:
  • Forgejo service account has read access to the data directory and config (always true by design)
Where this breaks in practice:
  • Encrypted secrets-at-rest (not default in Forgejo)
  • Network segmentation between Forgejo host and CI runners
Detection/coverage: File-integrity monitoring on app.ini and the repositories directory; anomalous outbound connections from the Forgejo host; git audit logs showing unexpected ref updates
03 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNo confirmed active exploitation as of 2026-09-12. Not listed in CISA KEV.
Proof-of-ConceptNo public PoC as of disclosure. However, the attack is mechanically simple — a skilled attacker can reproduce it from the advisory description alone within hours.
EPSS0.5% 30-day exploitation probability — low, but expected to rise quickly once a PoC surfaces given the trivial attack complexity.
KEV StatusNot listed. CISA KEV entry unlikely unless active exploitation is confirmed.
CVSS VectorCVSS: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 VersionsForgejo 16.0.0–16.0.3, Forgejo ≤ 15.0.7 (LTS), Gitea ≤ 1.25.4 (shared codebase)
Fixed VersionsForgejo 16.0.4, Forgejo 15.0.8 (LTS), Gitea 1.25.5 — all released 2026-09-10
Exposure DataForgejo 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 Date2026-09-10 — coordinated disclosure with patch release
ReporterReported to the Forgejo security team; specific researcher name not publicly credited in the advisory as of this assessment.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to CRITICAL (9.9/10)

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.

HIGH Vulnerability mechanism and severity assessment
HIGH Affected version ranges and patch availability
MEDIUM Exposure population and real-world instance count
LOW Active exploitation status (2 days post-disclosure, could change rapidly)

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.

05 · Compensating Control

What to do — in priority order.

  1. Patch to Forgejo 16.0.4 / 15.0.8 / Gitea 1.25.5 immediately — The patch removes any recreated .git directory after template variable expansion and before git 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).
  2. 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/create endpoint with template parameters. This eliminates the attack surface entirely until the patch is applied.
  3. Restrict repository creation to admin-approved users — Set DISABLE_REGISTRATION = true and 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.
  4. 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.
  5. 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.
What doesn't work
  • 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.
06 · Verification

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.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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 2
07 · Bottom Line

If you remember one thing.

TL;DR
Patch now. CVE-2026-89094 is a trivially exploitable RCE in the component that holds your source code — this is a supply-chain-class emergency. Under the noisgate mitigation SLA for CRITICAL, you have ≤ 3 days to apply a compensating control (disable template-repo creation or restrict repo-creation permissions) — that clock started on 2026-09-10, meaning your mitigation deadline is 2026-09-13. The noisgate remediation SLA gives you ≤ 90 days (by 2026-12-09) to confirm the vendor patch (Forgejo 16.0.4 / 15.0.8, Gitea 1.25.5) is deployed across all instances, but given the severity and simplicity of this bug, treat the patch itself as the mitigation and roll it out this weekend. Audit your Forgejo/Gitea instance list Monday morning, confirm every instance is on a patched version, verify self-registration is disabled, and scan template-derived repos for suspicious .git/hooks/ content that might indicate prior exploitation. If you also run Gitea — same bug, same urgency.

Sources

  1. Forgejo Release Notes — v16.0.4 and v15.0.8
  2. SecurityOnline — CVE-2026-89094 Forgejo RCE Analysis
  3. ByteIota — Forgejo CVE-2026-89094 CVSS 9.9 RCE
  4. LWN.net — Forgejo 16.0.4 and 15.0.8 Security Advisory
  5. THREATINT — CVE-2026-89094
  6. VulDB — CVE-2026-89094
  7. Forgejo Hardening Guide — Disable Self-Registration
  8. Lobsters Discussion — Forgejo 16.0.4 Critical Fix
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.