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.
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).
CVE-2026-89094 Severity
CVE-2026-89094 is rated critical severity.
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 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 Proof of Reproduction for CVE-2026-89094
- 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
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…
- POST /api/v1/repos/admin1/evil-template/generate
- repo_service.GenerateRepository
- GenerateGitContent
- generateRepoCommit (services/repository/generate_repo_commit.go): clone template, RemoveAll(.git), checkGiteaTemplate variable expansion renames .g${REPO_NAME}/config
- .git/config, git.InitRepository adopts recreated .git, initRepoCommit runs 'git commit' which executes attacker hook via…
How the agent worked
Root Cause and Exploit Chain for CVE-2026-89094
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 withservices/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 tagv16.0.3= commiteccddb2d17c93b42b2c8995725e03e549ac9ec0c). - 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-commithook executed asuid=1000(git)inside the Forgejo container during the generated repository's initial commit; the hook wrote an exact marker string, capturedidoutput, and read/etc/passwdfrom 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:
- Clones the template repository into a temp dir (
git.Clone, depth 1). - Removes the cloned
.git(util.RemoveAll(path.Join(tmpDir, ".git"))) before expansion. - 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), thenroot.Rename(relPath, substPath)). Since${REPO_NAME}is attacker-influenced (the name chosen for the generated repository), a template file named.g${REPO_NAME}/configis renamed to.git/configwhen the generated repo is namedit. - Runs
git.InitRepository(ctx, tmpDir, ...)—git initin a directory that already contains a.gitdirectory reinitializes it and preserves the attacker-writtenconfigand hooks. - Runs
git remote add,git add --all,git commit(initRepoCommit) — and git honors the adopted config.
Two execution vectors were validated:
core.fsmonitorin the adopted.git/configexecutes duringgit add --all(git appends version/token arguments to the configured command).- Repo-local
core.hooksPath = .ghooksoverrides Forgejo's globalcore.hooksPath = /var/lib/gitea/home/hooks(which is why a plain.git/hooks/post-commitfile is silently ignored), so the attacker's.ghooks/post-commit(an ordinary template file that rides along) executes duringgit 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
- Reference:
bundle/repro/reproduction_steps.sh(self-contained; requires Docker). - 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 createCLI and an API token. - Creates a repository
evil-templateviaPOST /api/v1/user/repos, marks it as a template viaPATCH /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(globsREADME.md,.g*/config,.ghooks/*),.g${REPO_NAME}/config(repo-localcore.hooksPath = .ghooks+core.fsmonitorfallback), executable.ghooks/post-commit(writes an exact marker string,idoutput, and/etc/passwdinto/tmpinside 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/generatewith{"name":"it","owner":"admin1","git_content":true}so that${REPO_NAME}=itturns.g${REPO_NAME}/configinto.git/config. - Collects per-attempt evidence: HTTP request/response transcripts, marker files, hook
idoutput, exfiltrated/etc/passwd, generated repo README, and the full Forgejo service log.
- Pulls/starts the official Forgejo container image, waits for
- Expected evidence of reproduction:
- Vulnerable attempts:
generatereturns 201;/tmp/marker-<tag>inside the container contains exactlyCVE-2026-89094-RCE-<tag>;/tmp/rce-id-<tag>containsuid=1000(git) ...;/tmp/hostdata-<tag>contains the container's/etc/passwd; generated repo still servesREADME.md==Hello!. - Fixed attempts (16.0.4): same attacker procedure; marker/hook/hostdata files absent; generation still succeeds with
README.md==Hello!.
- Vulnerable attempts:
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 bytesCVE-2026-89094-RCE-vuln-1/...-vuln-2written by the attacker hook.rce_id.txt—uid=1000(git) gid=1000(git) groups=1000(git)(command executed as the Forgejo runtime user).hostdata_etc_passwd.txt— 18 lines of/etc/passwdread from the Forgejo container host filesystem.forgejo_service.log— full product log including thePOST /api/v1/repos/admin1/evil-template/generate ... 201router line.template_tree.txt— git tree of the malicious template actually pushed (.g${REPO_NAME}/configmode 100644,.ghooks/post-commitmode 100755).
bundle/logs/fixed-1/,bundle/logs/fixed-2/— fixed control attempts:marker.txt/rce_id.txtcontain__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
.gitdirectory after template expansion and beforegit 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
TestRepoGenerateTemplatingDotGitDircovers the.git/configrecreation; add a case asserting that executable hook files andcore.hooksPath/core.fsmonitorvalues 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-commitpayload is silently ignored; the working payload overrides it with a repo-localcore.hooksPathpointing at an ordinary template directory (.ghooks).core.fsmonitorin the adopted config is an independent (also validated interactively) execution vector. - After a git push, the repository API's
emptyfield disappears (omitempty) once non-empty, so readiness must be polled viasize/raw-file availability — otherwise a generate issued too early is silently skipped (opts.GitContent && !templateRepo.IsEmpty).
- Forgejo sets a global
- 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.
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 -20total 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",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/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
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{"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]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 30200 404
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{"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.
How to Fix CVE-2026-89094
FAQ: CVE-2026-89094
Is CVE-2026-89094 exploitable?
How severe is CVE-2026-89094?
What type of vulnerability is CVE-2026-89094?
How can I reproduce CVE-2026-89094?
Is the CVE-2026-89094 reproduction verified?
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.