A Critical Forgejo RCE, and What It Really Exposes

Forgejo <=16.0.3 ships a critical RCE, patched in 16.0.4. Here's why your self-hosted forge is really a supply-chain control plane.

Share

Forgejo just shipped 16.0.4 to close a critical remote code execution hole affecting everything up to and including 16.0.3. If you run a self-hosted forge on a five-dollar VPS — and a surprising number of serious teams do — that box just became the most interesting machine you own.

Not because of the repositories. Because of everything the forge is trusted to hold.

Why this matters

Teams patch app servers weekly and forget the forge for years. It's "just Git," parked behind a login page nobody watches. But the forge is where your trust decisions live: deploy keys to production, CI runner tokens, webhook secrets, and the source every engineer pulls and runs. An RCE there isn't one server down — it's your build pipeline, executing attacker code with your name on the commits.

How it works

The pivot from "forge compromised" to "supply chain compromised" is embarrassingly short:

  • The RCE lands a shell as the forge's service user. Repos on disk are plain Git objects — readable, no extra auth required.
  • Config and database hold runner registration tokens and webhook secrets. Those are CI credentials with your cloud account attached.
  • One commit to a workflow file in the default branch, and your own CI executes attacker code under your deploy role.
  • Git's hashes protect history in transit — not from someone with admin on the server. Force-push the default branch or re-point a tag, and CI builds the rewrite while local clones just look "behind."

Note what didn't happen: no bug in your application, no phishing, no zero-day in your dependencies. You simply trusted the machine that feeds your builds.

Where this helps

  • The startup special: Forgejo on Hetzner, one shared runner with an AWS deploy role. One workflow edit turns the RCE into a backdoored artifact — or a quiet miner on your CI bill.
  • Agencies hosting client repos: shared runners, shared tokens. One compromise hits every client, because sharing tokens saved ten minutes of setup.
  • The internal library mirror: if the forge hosts your private packages, a re-pointed tag poisons every downstream build that trusts it.

Watch out

  • Verify what you actually run. Forgejo is a Gitea fork, and plenty of boxes still say Gitea on the login screen. Check forgejo --version or gitea --version before assuming anything.
  • Upgrading stops the bleeding; it doesn't undo it. If the box was touched, runner tokens, webhook secrets, and deploy keys are burned. Rotate them after the patch.
  • Detection is mostly manual: hunt for webhooks you don't recognize, runners you didn't register, and force-pushes on quiet branches.
  • Open user registration on an internet-facing forge isn't a feature — it's a waiting list for someone else's account. If contributors don't need the UI publicly, put it behind a VPN and keep SSH clones working.

Try it yourself

# What am I actually running?
docker exec forgejo forgejo --version   # or: gitea --version

# Affected (<= 16.0.3)? Pull the fix and restart.
docker compose pull && docker compose up -d

# Baseline your refs so silent rewrites stand out later.
git ls-remote origin | sha256sum > forge-baseline.sha256

# Then rotate: runner tokens, webhook secrets, deploy keys.

TL;DR

  • What changed: critical RCE in Forgejo, fixed in 16.0.4. Anything 16.0.3 or older is exposed.
  • Why it matters: the forge is your supply-chain control plane — an RCE there rides your own CI straight into production credentials.
  • Try today: upgrade, rotate runner tokens and webhook secrets, and save an ls-remote baseline to catch re-pointed tags.