← 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/Gitea Template Repository Variable Expansion to RCE

ASSESSED — NOISGATE
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 · Compensating Control

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.
04 · 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.

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
05 · The Call

Final Verdict
= UNCHANGED to CRITICAL (9.9/10)

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.

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