Forgejo 16.0.4 closes a critical RCE reachable from a single template repository
The self-hosted forge Forgejo shipped versions 16.0.4 and 15.0.8 on September 10 to fix a critical RCE born of template expansion, alongside three authorization flaws that share the same root cause. Everything on 16.0.3 or earlier must be patched, and fine-grained token inventories need a review.
September 10, 2026. Forgejo publishes versions 16.0.4 and 15.0.8 to fix CVE-2026-89094, a critical RCE triggered by variable expansion when generating a repository from a template. 16.0.3 and earlier are affected. The same release closes three more authorization flaws that, per the analysis by Reptile Haus, are the same bug wearing different clothes. For anyone self-hosting a forge, the real lesson extends past the patch: audit by object, not by endpoint.
An RCE built entirely out of correct steps
The mechanism is remarkable because none of the three steps is wrong on its own. When Forgejo generates a repository from a template repository, it chains together: clone the template, remove its .git folder, run variable expansion over the files listed in .forgejo/template, then run git init on the result.
Read that order again. The .git removal happens before expansion. The git init happens after. But expansion writes files: if a malicious template can coerce it into creating a .git folder of the attacker’s choosing, the git init that follows finds one already present — and git init on a directory that already contains a .git neither errors nor starts clean. It adopts what it finds, a documented and deliberate behavior.
A .git folder is not inert data: it carries a config and hooks, and several config keys nominate programs to execute. Forgejo itself describes the outcome as reading arbitrary data from the host and executing arbitrary processes on it. In practice, anyone who can publish a template repository your users generate from gets code execution on the machine holding every repository you own.
The fix changes none of the three steps: it removes .git a second time, after expansion — closing the gap between a control and a path that reaches past it.
Three authorization flaws, one root cause
The same release closes three permission defects that share a common origin: the check exists on some paths to an object, and not on others.
- The scoped token. Forgejo lets a maintainer edit branches they do not own when an open pull request carries the “allow maintainer edit” option. That special case was written against a logged-in human, before repository-scoped API tokens arrived. A token you had deliberately restricted could therefore reach past its restriction.
- The draft release. Draft releases are meant to be invisible without write access.
GetReleaseandListReleaseAttachmentscheck draft status before answering;GetReleaseAttachmentand the web download route did not. A reader — including an anonymous caller on a public repository — could fetch the full contents of an attachment that was never supposed to be visible. Forgejo flags this as the same class Gitea fixed upstream in GHSA-q9pg-jj6x-j9p6, a July advisory patched in Gitea 1.27.0. - The admin team. Filed under ordinary bug fixes rather than security: an organization team set to
adminwas actually being grantedownerpermission over its linked repositories. On a typical install, this is arguably the most consequential item in the release — it sits outside the security section only because nobody reported it as a vulnerability.
The common thread: each time, the control had been written correctly, but did not sit on the path an attacker would use.
Your scanner will not tell you about any of this
The most uncomfortable part lies elsewhere. A query against OSV, the database most scanners consult, returns 193 advisories under the code.gitea.io/gitea module path, and zero under forgejo.org, the path Forgejo actually publishes.
That is not a claim that Forgejo is more secure than Gitea: it is the absence of any claim, for lack of data. Forgejo publishes its security fixes as prose in release notes on Codeberg, frequently with no CVE at all. Your dependency scanner reads your lockfiles — and your forge is not in any lockfile. It is a container tag or a binary on a VM, and nothing in your pipeline compares its version against a feed that would fire.
This generalizes well past the project. The parts of your estate that automated scanning covers are the parts declared in a manifest. Everything installed as infrastructure sits outside that boundary: the forge, the CI runner, the artifact registry, the identity provider. Those are precisely the components whose compromise is not an availability incident but a supply-chain event — every artifact they produced afterward stays trusted downstream until proven otherwise.
The irony is that the people who need this warning most are the ones least likely to see it. A team that reads its Forgejo release notes already knows to patch; a team that does not — the team whose forge is running a two-year-old tag because “it still works” — will not see this article either. That is the structural problem with infrastructure security: the vulnerable path and the alerting path rarely intersect, so the fix has to be organizational before it is technical.
What to do this week
- Patch to 16.0.4 or 15.0.8. Both branches carry the fixes. Everything on 16.0.3 or earlier is affected, so the exposure window is however long your instance has been running, not the four weeks since the last release.
- Establish who can publish a template repository on your instance. If template creation is open to all, or the instance is public or multi-tenant, it is an untrusted input path. Inspect host processes and hook directories, not just access logs.
- Inventory your fine-grained tokens. For each, ask whether the restriction you configured is enforced on every route that reaches the resource, or only on the route you tested when you created it.
- Put every self-hosted service outside a lockfile on a named list with a named owner and a subscription to its release feed — forge, runner, registry, identity provider, secrets manager.
A sovereign forge, with the same blind spot
A useful reminder: Forgejo is the community fork of Gitea, born in late 2022 after the attempted privatization of Gitea by a company, and governed under Codeberg. Plenty of European teams self-host it precisely to keep their code off US infrastructure — which makes the tooling blind spot all the more ironic.
A self-hosted Forgejo is by definition a sovereignty asset: it holds all of your source code, your deployment secrets, and your history. Yet because it sits in no manifest, it escapes the scanners that watch your dependencies. The September 10 RCE simply makes concrete a risk many teams never mapped: the forge is the most valuable and least monitored piece of their infrastructure.
The fix is not technical, it is organizational: a named owner per self-hosted service, a subscription to its release feed, and a regular review. Everything else — patches, object-level audits — follows from there.
Verdict
Forgejo 16.0.4 is an immediate, mandatory fix for any self-hosted forge, but the patch is only half the job. The most serious flaw is not the RCE — it is the tooling blind spot: your forge sits in no manifest, so no scanner watches it.
If you self-host Forgejo, move to 16.0.4 or 15.0.8 now, then check who can create template repositories and audit your fine-grained tokens across all their routes.
If you run other self-hosted services outside lockfiles — Gitea, GitLab, a registry, a runner — apply the same rule: a named list with a named owner, and a subscription to the release feed. The gap nobody reads is the gap an attacker will find.