Skip to content

CVE-2026-89094: Verified Reproduction

CVE-2026-89094: Forgejo <16.0.4 RCE via crafted template repository .forgejo/template expansion recreates .git folder adopted by git init

CVE-2026-89094 is verified against the affected target. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00345.

REPRO-2026-00345 RCE Sep 11, 2026 CVE entry .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
49m 42s
Tool calls
257
Spend
$4.41
01 · Overview

What Is CVE-2026-89094?

CVE-2026-89094 is a critical-severity RCE vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00345).

02 · Severity & CVSS

CVE-2026-89094 Severity

CVE-2026-89094 is rated critical severity.

CRITICAL threat level
Weakness CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

How to Reproduce CVE-2026-89094

$ pruva-verify REPRO-2026-00345
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00345/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-89094

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Malicious template repository content pushed over git-HTTP: .forgejo/template glob list ('.g*/config', '.ghooks/*'), file .g${REPO_NAME}/config (git config with repo-local core.hooksPath=.ghooks and core.fsmonitor), and executable .ghooks/post-commit hook; triggered by POST /api/v1/repos/{owner}/{repo}/generate with n…

Attack chain
  1. POST /api/v1/repos/admin1/evil-template/generate
  2. repo_service.GenerateRepository
  3. GenerateGitContent
  4. generateRepoCommit (services/repository/generate_repo_commit.go): clone template, RemoveAll(.git), checkGiteaTemplate variable expansion renames .g${REPO_NAME}/config
  5. .git/config, git.InitRepository adopts recreated .git, initRepoCommit runs 'git commit' which executes attacker hook via…
How the agent worked 538 events · 257 tool calls · 50 min
50 minDuration
257Tool calls
104Reasoning steps
538Events
11Dead-ends
Agent activity over 50 min
Policy
1
Support
15
Repro
295
Judge
33
Variant
189
Verify
1
0:0049:32

Root Cause and Exploit Chain for CVE-2026-89094

Versions: Forgejo < 16.0.4 (verified on the official container image codeberg.org/forgejo/forgejo:16.0.3-rootless, source tag v16.0.3 = commit eccddb2d17c93b42b2c8995725e03e549ac9ec0c).

When a user generates a new repository from a template repository, Forgejo clones the template, deletes the .git directory, applies variable template expansion to the files listed in .forgejo/template (expanding variables in both file content and file paths, e.g. .g${REPO_NAME}/config.git/config when the generated repository is named it), and then runs git init in that working directory. Because no cleanup happens after the expansion step, an attacker-controlled template can recreate a .git directory during expansion, and the subsequent git init adopts it. Forgejo then performs git add --all and git commit in that directory, honoring the attacker-supplied .git/config (e.g. core.fsmonitor, core.hooksPath) and hooks, which leads to arbitrary command execution on the Forgejo host as the Forgejo runtime user. The fix (PR #14301, released in v16.0.4) removes any .git directory again after the expansion completes and before git init.

  • Package/component affected: Forgejo services/repository/generate_repo_commit.go (repository generation from template), interacting with services/repository/generate.go (variable expansion).
  • Affected versions: Forgejo < 16.0.4 (verified on the official container image codeberg.org/forgejo/forgejo:16.0.3-rootless, source tag v16.0.3 = commit eccddb2d17c93b42b2c8995725e03e549ac9ec0c).
  • Risk level: Critical (CVSS 3.1 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H = 9.9). Any authenticated user who can create a template repository and generate a repository from it obtains arbitrary command execution in the Forgejo container/host as the Forgejo runtime user, plus arbitrary read (and write) of host data reachable by that user.

Impact Parity

  • Disclosed/claimed maximum impact: Remote code execution on the Forgejo host via a crafted template repository ("read arbitrary data from the Forgejo host and execute arbitrary processes, e.g. via git hooks/config such as core.fsmonitor or hooks").
  • Reproduced impact from this run: Full remote code execution through the real product HTTP API: attacker-supplied post-commit hook executed as uid=1000(git) inside the Forgejo container during the generated repository's initial commit; the hook wrote an exact marker string, captured id output, and read /etc/passwd from the host filesystem (arbitrary host data read). Both the vulnerable (16.0.3) and fixed (16.0.4) builds were exercised end-to-end.
  • Parity: full
  • Not demonstrated: Nothing material; the claimed impact (arbitrary process execution + host data read) was demonstrated directly.

Root Cause

In services/repository/generate_repo_commit.go (v16.0.3), generateRepoCommit:

  1. Clones the template repository into a temp dir (git.Clone, depth 1).
  2. Removes the cloned .git (util.RemoveAll(path.Join(tmpDir, ".git"))) before expansion.
  3. Reads .forgejo/template (or .gitea/template) and, for every file matching one of its globs, expands variables in the content (generateExpansion(content, ..., false)) and in the path (generateExpansion(relPath, ..., true), then root.Rename(relPath, substPath)). Since ${REPO_NAME} is attacker-influenced (the name chosen for the generated repository), a template file named .g${REPO_NAME}/config is renamed to .git/config when the generated repo is named it.
  4. Runs git.InitRepository(ctx, tmpDir, ...)git init in a directory that already contains a .git directory reinitializes it and preserves the attacker-written config and hooks.
  5. Runs git remote add, git add --all, git commit (initRepoCommit) — and git honors the adopted config.

Two execution vectors were validated:

  • core.fsmonitor in the adopted .git/config executes during git add --all (git appends version/token arguments to the configured command).
  • Repo-local core.hooksPath = .ghooks overrides Forgejo's global core.hooksPath = /var/lib/gitea/home/hooks (which is why a plain .git/hooks/post-commit file is silently ignored), so the attacker's .ghooks/post-commit (an ordinary template file that rides along) executes during git commit.

Fix: PR #14301 "fix: prevent template expansion from interfering with git initialization" (commit 0d74280e071a9ddc145930f93fa7b5cb50c2a4e8, released in v16.0.4, tag 6e56b5ebad3fb05036b1ff68a6b47b80f5859c7c) adds, after the expansion loop and before git.InitRepository:

// Before template expansion, .git was removed so that a fresh repo can be initialized; remove it again in case
// some template variable usage has conflicted with this directory and impacts git operations.
if err := root.RemoveAll(".git"); err != nil {
    return fmt.Errorf("unable to remove .git folder")
}

Reproduction Steps

  1. Reference: bundle/repro/reproduction_steps.sh (self-contained; requires Docker).
  2. What the script does, per attempt (2× vulnerable 16.0.3-rootless, 2× fixed 16.0.4-rootless, each a fresh container on its own port):
    • Pulls/starts the official Forgejo container image, waits for /api/healthz.
    • Creates the admin user via the real forgejo admin user create CLI and an API token.
    • Creates a repository evil-template via POST /api/v1/user/repos, marks it as a template via PATCH /api/v1/repos/admin1/evil-template ({"template":true}).
    • Builds and pushes the malicious template over the real git-HTTP receive path: README.md, .forgejo/template (globs README.md, .g*/config, .ghooks/*), .g${REPO_NAME}/config (repo-local core.hooksPath = .ghooks + core.fsmonitor fallback), executable .ghooks/post-commit (writes an exact marker string, id output, and /etc/passwd into /tmp inside the container).
    • Waits until the template content is registered, then triggers the vulnerability through the real product workflow: POST /api/v1/repos/admin1/evil-template/generate with {"name":"it","owner":"admin1","git_content":true} so that ${REPO_NAME} = it turns .g${REPO_NAME}/config into .git/config.
    • Collects per-attempt evidence: HTTP request/response transcripts, marker files, hook id output, exfiltrated /etc/passwd, generated repo README, and the full Forgejo service log.
  3. Expected evidence of reproduction:
    • Vulnerable attempts: generate returns 201; /tmp/marker-<tag> inside the container contains exactly CVE-2026-89094-RCE-<tag>; /tmp/rce-id-<tag> contains uid=1000(git) ...; /tmp/hostdata-<tag> contains the container's /etc/passwd; generated repo still serves README.md == Hello!.
    • Fixed attempts (16.0.4): same attacker procedure; marker/hook/hostdata files absent; generation still succeeds with README.md == Hello!.

Evidence

All artifacts under bundle/ (SHA-256 digests in bundle/repro/runtime_manifest.json):

  • bundle/logs/vuln-1/, bundle/logs/vuln-2/ — vulnerable attempts:
    • http_generate_request.txt, http_generate_response.json, http_generate_status.txt — the triggering API call (HTTP 201).
    • marker.txt — exact bytes CVE-2026-89094-RCE-vuln-1 / ...-vuln-2 written by the attacker hook.
    • rce_id.txtuid=1000(git) gid=1000(git) groups=1000(git) (command executed as the Forgejo runtime user).
    • hostdata_etc_passwd.txt — 18 lines of /etc/passwd read from the Forgejo container host filesystem.
    • forgejo_service.log — full product log including the POST /api/v1/repos/admin1/evil-template/generate ... 201 router line.
    • template_tree.txt — git tree of the malicious template actually pushed (.g${REPO_NAME}/config mode 100644, .ghooks/post-commit mode 100755).
  • bundle/logs/fixed-1/, bundle/logs/fixed-2/ — fixed control attempts: marker.txt/rce_id.txt contain __ABSENT__, generated_readme.txt == Hello!, http_generate_status.txt == HTTP 201.
  • bundle/repro/runtime_manifest.json — runtime stack, image digests (sha256:214f4ae6... vulnerable / sha256:a263a129... fixed), source commits, and artifact digests.
  • bundle/logs/reproduction_steps.log — console transcript of both verification runs.

Environment: Docker on linux/x86_64; images codeberg.org/forgejo/forgejo:16.0.3-rootless (source v16.0.3, eccddb2d17c93b42b2c8995725e03e549ac9ec0c) and codeberg.org/forgejo/forgejo:16.0.4-rootless (source v16.0.4, 6e56b5ebad3fb05036b1ff68a6b47b80f5859c7c); Forgejo runs sqlite3; repo push over git-HTTP with a token-authenticated admin session.

Recommendations / Next Steps

  • Upgrade to Forgejo v16.0.4 (or later), which removes any .git directory after template expansion and before git init (PR #14301).
  • Defense in depth for deployers: none of Forgejo's process-level settings prevent this once the bug is present; upgrading is the fix. The execution lands as the Forgejo runtime user — containerize/isolate and drop unneeded privileges.
  • Testing recommendations: the upstream integration test TestRepoGenerateTemplatingDotGitDir covers the .git/config recreation; add a case asserting that executable hook files and core.hooksPath/core.fsmonitor values delivered through template expansion never influence the generated repository's git operations.

Additional Notes

  • Idempotency: the script was executed twice consecutively; both runs ended with VERDICT: CONFIRMED (exit 0). It removes containers/data dirs by name before each attempt, so repeated runs are clean.
  • Two important mechanics discovered while validating:
    • Forgejo sets a global core.hooksPath (/var/lib/gitea/home/hooks) for its git subprocesses, so a naive .git/hooks/post-commit payload is silently ignored; the working payload overrides it with a repo-local core.hooksPath pointing at an ordinary template directory (.ghooks). core.fsmonitor in the adopted config is an independent (also validated interactively) execution vector.
    • After a git push, the repository API's empty field disappears (omitempty) once non-empty, so readiness must be polled via size/raw-file availability — otherwise a generate issued too early is silently skipped (opts.GitContent && !templateRepo.IsEmpty).
  • Limitations: the marker/hook evidence is collected from the container filesystem via docker exec (the attacker-relevant boundary — template push + generate — is fully remote over HTTP). The execution user is the containerized Forgejo runtime user (uid=1000 git), not root, which matches the disclosed impact scope.

CVE-2026-89094 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:36
0:00
session startedaccounts/fireworks/models/glm-5p3 · CVE-2026-89094 · REPRO-20
0:02
0:03
web search
0:05
web search
0:08
0:09
0:27
0:28
web search
1:09
1:09
extract_facts
no facts extracted
1:10
1:10
supportclaim_contract
1:17
1:17
1:17
1:18
1:18
1:18
1:21
1:21
1:21
1:23
$ls -la /pruva/project-cache/repo-mirrors/ && cat /pruva/project-cache/.pruva/cache_manifest.json && ls /pruva/project-cache/repo-mirrors/* 2>/dev/null | head -20
0.4s
total 0
drwx------ 2 vscode vscode 40 Sep 11 08:23 .
drwx------ 4 vscode vscode 80 Sep 11 08:23 ..
{
  "schema_version": 1,
  "entries": [
    {
      "path": "repo-mirrors",
1:24
$which go docker git jq curl node python3; go version 2>/dev/null; docker ps 2>&1 | head -3; nproc; df -h /pruva /workspace | tail -3
0.5s
/usr/bin/docker
/usr/local/bin/git
/usr/bin/jq
/usr/bin/curl
/usr/bin/node
/usr/bin/python3
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
4
1:30
1:31
$git ls-remote --tags https://codeberg.org/forgejo/forgejo.git 2>&1 | tail -20; echo "---"; git ls-remote https://github.com/forgejo/forgejo.git HEAD 2>&1 | head -3
1.3s
{"cwd":"/workspace","duration_ms":715,"exit_code":0,"stderr":"","stdout":"f571085e19a33143db48a33667f7e3492c1aeee8\trefs/tags/v7.0.15\n10343f761a03af57714d778e3a0e99a6db399870\trefs/tags/v7.0.16\nad0b97c7c26072d3ddbf76c55c3edaab2f070a2b\trefs/tags/v7.0.2\nb5c49a19d2ae8721e29adcbb1b6b624b150c0e10\trefs/tags/v7.0.3\n5c59a1347a5fbf183eef22fe46805b4d1a8606f3\trefs/tags/v7.0.4\n65a89ce6be62018ea50cb01c080072973ce87efb\trefs/tags/v7.0.5\nc47bdf436b6c7dc43933560a65763fb23a7ca5c9\trefs/tags/v7.0.6\n29afb54dafab1533a0be696f47a61f26fcf3be96\trefs/tags/v7.0.7\n47cd797dd3546c9728234196bb8330550c40ca61\trefs/tags/v7.0.8\n7644435aed0ed84ce6b4ad8887807df6184d75ff\trefs/tags/v7.0.9\n4d2263e82e1b6a11213f43be67e49b8f15097d81\trefs/tags/v8.0.0\n0761207cc2317d99475076d8ce569865d9f48fb0\trefs/tags/v8.0.0-dev\n4d0be867a22caccf38c1a776de5502a43dedcf02\trefs/tags/v8.0.1\n949e415fd3c90f771c34195312ae74735f760add\trefs/tags/v8.0.2\n71658e29530f275db8f4a431387d334af8e80677\trefs/tags/v8.0.3\n12a277ed65220cc152419242280f426e84ba0e61\trefs/tags/v9.0.0\n9524361bb43f94ff48ba32ba5bb14f1002bb24cf\trefs/tags/v9.0.0-dev\n1dc03cc1c301f89a99728e4e5ea144f4aeab1da1\trefs/tags/v9.0.1\n6569f1f25fd428adf14368ce5ad50d8049df… [truncated]
1:32
$curl -s "https://codeberg.org/forgejo/forgejo/releases/tag/v16.0.3" -o /dev/null -w "%{http_code}\n" --max-time 30; curl -s "https://dl.gitea.com/forgejo/forgejo/16.0.3/forgejo-16.0.3-linux-amd64" -o /dev/null -w "%{http_code}\n" --max-time 30
0.8s
200
404
1:35
1:36
$git ls-remote --tags https://codeberg.org/forgejo/forgejo.git 2>&1 | grep -E "v1[0-9]\." | sort -V | tail -30; echo "---refs---"; git ls-remote https://codeberg.org/forgejo/forgejo.git HEAD refs/heads/forgejo* refs/heads/release* 2>&1 | head -20
1.3s
{"cwd":"/workspace","duration_ms":824,"exit_code":0,"stderr":"","stdout":"52406dc6eaf3bf31d8cd04fef329583f476280d5\trefs/tags/v11.0.9\n308810cdd17e3edf928d38c5d410acf2c7aa7618\trefs/tags/v11.0.0\n661028623c7876bc112deff80239d5ea199dcff8\trefs/tags/v11.0.1\na1b1a63549c4dc9b1da31378e677fa9ca1574ed3\trefs/tags/v11.0.6\na3a52251ad892e1932d5a7f50b0b7c5bb186eac5\trefs/tags/v14.0.0\na32151caf6b539640578f203a6feb51bb5603160\trefs/tags/v11.0.15\na5951616e8f91e8c42df5d2d888c446f710d9bdf\trefs/tags/v11.0.7\naaf7960a77e8964d6ec08a3870ca25ac49561a6a\trefs/tags/v13.0.3\nafb5825b73bc486e600e1204b526359a524bd292\trefs/tags/v11.0.8\nb3d7e4ac3cbccc220703097a51fa4c16bf302579\trefs/tags/v16.0.1\nb81aa4d0807cc1e2d6a2fbafd0b75c46cc0ae237\trefs/tags/v11.0.3\nb83d564113ba0b5371c1fc202b22ade5ad7ee64f\trefs/tags/v11.0.12\nb8448e7cde8f56a4b769fa74e25b18971a6c1304\trefs/tags/v13.0.2\nbcd0821f3ef01c92c29a53dd4d2fb4472e4cc162\trefs/tags/v12.0.1\nc6a1d64dc138902f81d21691c952d67d427e2fd4\trefs/tags/v16.0.0-dev\nc72fd88d3593880d408ce0533a3da63c45653df2\trefs/tags/v11.0.2\nca46a3f68b8fac02098924aeab61ea6c2850bf11\trefs/tags/v14.0.1\ncd7f92797b6ce3191ffec9524d7e2e979f94b812\trefs/tags/v10.0.0-dev\nd0dec3d857868e8e71… [truncated]

Artifacts and Evidence for CVE-2026-89094

Scripts, logs, diffs, and output captured during the reproduction.

bundle/logs/fixed-1/forgejo_service.log8.8 KB
bundle/logs/fixed-1/generated_readme.txt0.0 KB
bundle/logs/fixed-1/hostdata_etc_passwd.txt0.0 KB
bundle/logs/fixed-1/http_create_repo_status.txt0.0 KB
bundle/logs/fixed-1/http_generate_request.txt0.1 KB
bundle/logs/fixed-1/http_generate_response.json2.2 KB
bundle/logs/fixed-1/http_generate_status.txt0.0 KB
bundle/logs/fixed-1/http_mark_template_status.txt0.0 KB
bundle/logs/fixed-1/marker.txt0.0 KB
bundle/logs/fixed-1/rce_id.txt0.0 KB
bundle/logs/fixed-1/template_tree.txt0.3 KB
bundle/logs/fixed-2/forgejo_service.log8.8 KB
bundle/logs/fixed-2/generated_readme.txt0.0 KB
bundle/logs/fixed-2/hostdata_etc_passwd.txt0.0 KB
bundle/logs/fixed-2/http_create_repo_status.txt0.0 KB
bundle/logs/fixed-2/http_generate_request.txt0.1 KB
bundle/logs/fixed-2/http_generate_response.json2.2 KB
bundle/logs/fixed-2/http_generate_status.txt0.0 KB
bundle/logs/fixed-2/http_mark_template_status.txt0.0 KB
bundle/logs/fixed-2/marker.txt0.0 KB
bundle/logs/fixed-2/rce_id.txt0.0 KB
bundle/logs/fixed-2/template_tree.txt0.3 KB
bundle/logs/vuln-1/generated_readme.txt0.0 KB
bundle/logs/vuln-1/http_create_repo_status.txt0.0 KB
bundle/logs/vuln-1/http_generate_status.txt0.0 KB
bundle/logs/vuln-1/http_mark_template_status.txt0.0 KB
bundle/logs/vuln-1/template_tree.txt0.3 KB
bundle/logs/vuln-2/forgejo_service.log8.8 KB
bundle/logs/vuln-2/generated_readme.txt0.0 KB
bundle/logs/vuln-2/hostdata_etc_passwd.txt0.7 KB
bundle/logs/vuln-2/http_create_repo_status.txt0.0 KB
bundle/logs/vuln-2/http_generate_request.txt0.1 KB
bundle/logs/vuln-2/http_generate_response.json2.2 KB
bundle/logs/vuln-2/http_generate_status.txt0.0 KB
bundle/logs/vuln-2/http_mark_template_status.txt0.0 KB
bundle/logs/vuln-2/rce_id.txt0.0 KB
bundle/logs/vuln-2/template_tree.txt0.3 KB
bundle/repro/rca_report.md10.3 KB
bundle/repro/reproduction_steps.sh15.9 KB
bundle/repro/runtime_manifest.json8.2 KB
bundle/repro/validation_verdict.json1.9 KB
08 · How to Fix

How to Fix CVE-2026-89094

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-89094 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-89094

Is CVE-2026-89094 exploitable?

Yes. Pruva independently reproduced CVE-2026-89094 and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00345).

How severe is CVE-2026-89094?

CVE-2026-89094 is rated critical severity.

What type of vulnerability is CVE-2026-89094?

CVE-2026-89094 is classified as CWE-22 (Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')), a RCE vulnerability.

How can I reproduce CVE-2026-89094?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-89094 reproduction verified?

Yes. Pruva reproduced CVE-2026-89094 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-89094

Authoritative sources for CVE-2026-89094 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.