Every developer who has created a project knows the flow. Click "new repository", pick a template, and the forge copies a starter repo for you, maybe swapping in your project name where the template says {{ REPO_NAME }}. It is the least suspicious button in the whole interface.
On September 10, 2026, the Forgejo team released 16.0.4 to fix a critical bug hiding behind exactly that button. Versions before 16.0.4 were vulnerable to remote code execution through a crafted template repository, tracked as CVE-2026-89094 with a CVSS score of 9.9. A malicious template could read arbitrary data from the server and execute arbitrary processes on it. The bug hit the Hacker News front page this week with over 200 points, and the comment thread turned into a debate about what "RCE" even means, which tells you how unusual the attack path is.
Everything below comes from the Forgejo security announcement, the release notes for 16.0.4 and 15.0.8, the GitHub advisory, and LWN's coverage, with each claim attached to its source. What I can add is the part most coverage skips: the fix is two lines of ordering, the same failure pattern shows up twice more in the same release, and if you run any self-hosted forge, there is a concrete checklist for this week.
The bug is an ordering mistake
When you generate a repository from a template, Forgejo does four things in sequence. The release notes spell them out:
- Clone the template repository.
- Remove the
.gitfolder. - Perform variable template expansion on the files listed in
.forgejo/template. - Initialize a new git repository.
Read that list again and look for the problem. The .git folder is deleted at step two, but step three writes files. And template expansion writes wherever the template tells it to.
That means a crafted template could use variable expansion to create a new .git folder inside the working directory, one full of attacker-controlled git metadata. Then step four runs git init, and git does not start from zero: it adopts whatever repository structure it finds. The attacker's metadata gets incorporated into the new repository, and from there the release notes are blunt. A malicious template repository could read arbitrary data from the Forgejo host and execute arbitrary processes on it.
One sentence from the release notes deserves to be framed, because it is the entire vulnerability class in miniature: after variable expansion is completed, any existing .git folder is removed from the directory before the git repository is initialized. The fix moves the cleanup from before the dangerous step to after it. That is it. No new library, no sandbox, no architecture change. Delete the folder last, not first.
Why this qualifies as RCE. The Hacker News thread spent dozens of comments arguing about this. The attack requires authentication: you need an account on the instance and permission to create a repository from a template. Some argued that makes it not really remote code execution. That argument misses how these systems actually fail. A forge's threat model is exactly "one user should not be able to become the server." If a regular authenticated user can turn a repo-creation click into shell-level access on the host, that is user-to-server privilege escalation, and on public instances like Codeberg, where anyone can register, it is remote in every sense that matters. One commenter put the practical stakes simply: if an attacker takes over any single user account on your instance, this bug lets them escalate from that account to the whole host.
The same bug shape appears twice more in the same release
This is the part I find genuinely instructive, because it turns one CVE into a review technique. The 16.0.4 release fixed three security issues, and the other two fail in exactly the same way as the template bug.
The twin-route problem. Users with only read permission, including unauthenticated visitors on public repositories, could retrieve the metadata and full contents of attachments on draft releases. The GetReleaseAttachment API endpoint and the web attachment download route never checked whether the release was a draft, while the sibling endpoints GetRelease and ListReleaseAttachments did. Draft releases were hidden everywhere except the one path that served the actual bytes. There is history here too: Gitea fixed the API surface in February 2026 as CVE-2026-27660, but the web-level UUID routes were missed and reported separately as CVE-2026-58432, and the UUID is embedded in the browser_download_url the API hands out. The same bug found three times, across two codebases.
The twin-auth problem. Forgejo lets maintainers edit branches on an open pull request when "allow maintainer edit" is set. That control did not account for API-specific restrictions like repo-scoped access tokens, so a token with deliberately limited permissions could modify branches outside its restriction. The override worked for session authentication and was never reconsidered for tokens.
Three fixes, one failure mode: a rule enforced on one code path but not its twin. Cleanup on one side of a step but not the other. A permission check on the API route but not the web route. A restriction honored for sessions but not tokens. None of these is an exotic vulnerability class. All of them survive code review because the reviewer is looking at the path in front of them instead of the set of paths that reach the same resource.
The five-minute audit for your own Forgejo
If you run Forgejo, Gitea, or any self-hosted forge, here is what I would do this week, in order. None of it requires the template bug specifically; it is just good hygiene that this incident makes urgent.
1. Check your version right now. The version shows in the admin panel, or in the page footer, or via the API:
curl -s https://your-forge.example.com/api/v1/version
If it reports anything below 16.0.4 (or below 15.0.8 on the 15.x line), you are in the affected window. The vulnerable releases were current as of early September 2026, so a forge updated on any normal cycle before September 10 is exposed.
2. Upgrade. There is no configuration mitigation for the template flaw short of disabling repository generation from templates entirely. On Docker, pull the fixed image and recreate the container. The official image lives at codeberg.org/forgejo/forgejo on Forgejo's own registry:
docker pull codeberg.org/forgejo/forgejo:16.0.4
docker compose down && docker compose up -d
Forgejo runs its database migrations automatically on startup, including across skipped releases, but back up /data and the database first. The upgrade guide's own advice: if you cannot reach codeberg.org, the same images are mirrored at data.forgejo.org.
3. Assume draft releases were not private. If your team staged builds or unreleased binaries in draft releases on an affected version, treat those artifacts as disclosed to anyone who could obtain an attachment UUID. Patching does not un-leak a URL that was already shared. Rotate anything sensitive those artifacts contained: embedded tokens, signing keys, internal hostnames.
4. Audit repo-scoped tokens. The maintainer-edit bypass means a restricted token may have been able to write outside its scope. Rotating tokens is cheap. Reconstructing what a token did six weeks ago is not. Check the instance logs for branch modifications by tokens where the ref does not match the token's scope.
5. Check who can create templates. Template repositories are the attack vehicle here. On instances with open registration, any user could in principle publish a template that other users might pick up. Review which repos are marked as templates, and if your instance does not need public template discovery, restrict it.
The broader lesson: expansion output is not data
The transferable lesson outlives the patch. Repository templating looks like a convenience feature and behaves like a code execution surface. Content arrives from outside your trust boundary, your server expands it, and the expansion writes to a filesystem that your own tooling then interprets. That is structurally identical to any injection bug, with a filesystem in place of a string parser.
Once you see the shape, you start finding it in your own systems. Ask one question: what content does a user supply that my server writes to disk and then acts on?
- Uploaded archives that get extracted, where extraction can write paths outside the target directory.
- Configuration files that are merged and then fed to a loader with interpolation enabled.
- Anything generated from a template, where the template controls where the output lands.
- The 2026 version: comments and file contents that become instructions for an AI coding agent with write access.
In each case, the order of cleanup and expansion decides whether you have a feature or an exploit. The Forgejo fix is a good mnemonic for the whole class: do the dangerous write first, then clean up, then let your tooling look at the result. Never the reverse.
The last recurring finding deserves its own paragraph. Self-hosted developer infrastructure tends to be installed once, by whoever needed webhook delivery, and then inherited. It holds every line of source you own and often the credentials to deploy it, and it gets patched on a slower cycle than anything customer-facing because nothing breaks when you delay. One HN commenter's tally matched my experience of how people talk about this: the total time spent maintaining his Forgejo instance this year was still less than the time GitHub spent degraded. Self-hosting is a reasonable choice. Treating the forge as a production system, with the same upgrade and exposure review as everything customer-facing, is the part teams skip.
I write about developer tools, security, and backend engineering every week. Subscribe, it is free, and you will get the next one when the next critical CVE lands.
Now the question for you: do you run a self-hosted forge, and did this CVE reach it before or after the HN thread did? If you checked your version after reading this, tell me what you found in the comments.
Sources:
- Forgejo security announcement for v15.0.8 and v16.0.4: codeberg.org/forgejo/security-announcements #59
- Release notes for 16.0.4 and 15.0.8
- GitHub advisory CVE-2026-89094: GHSA-q873-4w8p-m645
- LWN coverage: Forgejo 16.0.4 and 15.0.8 address critical security vulnerability
- Hacker News discussion: Forgejo <=16.0.3 Critical RCE
Top comments (0)