Carrot disclosure: Forgejo

3 min read Original article ↗

Since Fedora moved from Pagure to Forgejo, I finally had an incentive to take a good look at Forgejo'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, information leak all over the place, 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 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, 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, 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 I wanted, 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 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. Users of this disclosure model are of course called Bugs Bunnies.

So without further ado:

$ 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.