Title: Carrot disclosure: Forgejo
Date: 2026-04-28 22:40

Since Fedora [moved from Pagure to
Forgejo](https://communityblog.fedoraproject.org/the-forge-is-our-new-home/), I
finally had an incentive to take a good look at
[Forgejo](https://forgejo.org/)'s security posture. The results aren't pretty
to be honest: SSRF in a lot of places, no CSP/Trusted-Types, a bit of ghetto templating in
javascript, cryptographic malpractices, overlooks in the authentication
mechanisms (OAuth2, OTP, sessions/access handling, post-compromission recovery,
…), a bunch of low-hanging DoS, some information leaks, various
TOCTOU, … All in all, it took me one evening after work to find a good amount
of vulnerabilities (adding to the one I got from looking at
[gitea](https://about.gitea.com/) at some point in the past), and chain some of
them to obtain a full-blown RCE, some secrets leaks, a bunch of persistent
account access, a handful of OAuth2 privesc, …

Fortunately (or unfortunately depending who you're asking), the RCE relies on
open registration, and on a configuration option set to a non-default value
(which is the case on some instances I've looked at, so nothing exotic),
meaning that its selling value is pretty low/nonexistent. I could disclose the
bugs to Forgejo, they even have a [Security
Policy](https://codeberg.org/forgejo/governance/src/branch/main/SECURITY-POLICY.md),
with a lot of `MUST`/`MUST NOT` about what I must or mustn't do should I decide
to go this way. But given the sorry state of the codebase (not their fault
though, they inherited the gitea/gogs ones), I'm pretty sure I
could spend another evening and find another chain, and odds are that others
have a bunch as well. I could try to fix the issues one by one myself and send
pull-requests, but even [if](https://codeberg.org/forgejo/forgejo/pulls/12288)
[I](https://codeberg.org/forgejo/forgejo/pulls/12285)
[wanted](https://codeberg.org/forgejo/forgejo/pulls/12283), this is a
systemic issue, there is little point in playing endless wack-a-mole.

I discussed the conundrum with a friend of mine, and was told to put my money
where my mouth is, and just go with [carrot
disclosure]({filename}/security/carrot_disclosure.md) that I usually advocate
for in this kind of situation:

> Carrot Disclosure, dangling a metaphorical carrot in front of the vendor to
incentivise change. The main idea is to only publish the (redacted) output of
the exploit for a critical vulnerability, to showcase that the software is
exploitable. Now the vendor has two choices: either perform a holistic audit of
its software, fixing as many issues as possible in the hope of fixing the
showcased vulnerability; or losing users who might not be happy running a
known-vulnerable software.

So without further ado:

```console
$ python3 ./chain_alpha.py --target http://127.0.0.1:3000 > out.txt
$ grep Backdoor out.txt 
[+]   Backdoor admin created: svc_ljeopgid / dukecepapsygiqks!A1
$ tail -n17 out.txt 

================================================================
[+] COMMAND EXECUTION CONFIRMED!
================================================================

Server-side hook output (received via git push stderr):

  remote: ==========================================
  remote: FORGEJO RCE PoC - Command Execution Proof
  remote: ==========================================
  remote: hostname: chernabog
  remote: uid:      uid=1000(jvoisin) gid=1000(jvoisin) groups=1000(jvoisin),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
  remote: date:     Tue Apr 28 19:16:59 UTC 2026
  remote: proof:    chernabog
  remote: ==========================================

================================================================
$ sha256 ./chain_alpha.py
c10d28a5ff74646683953874b035ca6ba56742db2f95198b54e561523e1880d7  ./chain_alpha.py
jvoisin@chernabog 11:35 ~/Documents/exploits/forgejo tree
.
├── chain_alpha.py
├── chain_beta.py
├── chain_gamma.py
├── dos
│   ├── cpuburn_authenticated.py
│   ├── cpu_dos.py
│   ├── dbburn.py
│   ├── dfburn.py
│   ├── exhaust.py
│   ├── gburn.py
│   ├── grpstarve.py
│   ├── rstarve.py
│   ├── starve.py
│   └── storage.py
├── f9_repo_settings.py
├── get_version.py
├── leak_secrets.py
├── leak_token.py
├── merge.py
└── NOTES.md

2 directories, 19 files
$
```

[edit] The corresponding Mastodon post was removed as "Some of your posts have
been found to violate one or more community guidelines and have been
subsequently removed by the moderators of infosec.exchange.", but it seems to
have been a mistake, it's [now back](https://infosec.exchange/@jvoisin/116488420408417722).
