Carrot Disclosure: Forgejo
Source: Hacker News
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: SSRF in many places, no CSP/Trusted‑Types, a bit of ghetto templating in JavaScript, cryptographic malpractices, oversights in the authentication mechanisms (OAuth2, OTP, session/access handling, post‑compromise recovery, …), a bunch of low‑hanging DoS vectors, information leaks all over the place, various TOCTOU issues, …
All in all, it took me one evening after work to find a good amount of vulnerabilities (adding to the ones I discovered in Gitea in the past), and chain them to obtain a full‑blown RCE, several secret leaks, persistent account access, and a handful of OAuth2 privilege‑escalations.
Fortunately (or unfortunately, depending on who you ask), 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 examined, so nothing exotic). This limits its selling value. I could disclose the bugs to Forgejo—they even have a Security Policy with many MUST/MUST NOT clauses—but given the sorry state of the codebase, I’m confident I could spend another evening finding another chain. I could fix the issues myself and send pull‑requests, but the effort required is non‑trivial.
After discussing the conundrum with a friend, I decided to use carrot disclosure, a model I usually advocate for in this kind of situation.
Carrot Disclosure
Carrot disclosure involves dangling a metaphorical carrot in front of the vendor to incentivise change. The main idea is to publish only the redacted output of an exploit for a critical vulnerability, demonstrating that the software is exploitable. The vendor then has two choices:
- Perform a holistic audit of the software, fixing as many issues as possible in the hope of addressing the showcased vulnerability.
- Lose users who are unwilling to run known‑vulnerable software.
Users of this disclosure model are, of course, called Bugs Bunnies.
Proof of Concept
Below is a trimmed excerpt of the exploit output that confirms command execution on a vulnerable Forgejo instance.
$ python3 ./poc/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: ==========================================
================================================================
Verification
$ sha256 ./poc/chain_alpha.py
c10d28a5ff74646683953874b035ca6ba56742db2f95198b54e561523e1880d7 ./poc/chain_alpha.py
Repository Layout
$ ls -l ./poc
total 140
-rw-r--r--. 1 jvoisin jvoisin 23530 Apr 28 21:18 chain_alpha.py
-rw-r--r--. 1 jvoisin jvoisin 6382 Apr 28 01:14 chain_beta.py
-rw-r--r--. 1 jvoisin jvoisin 11410 Apr 28 21:54 chain_gamma.py
-rw-r--r--. 1 jvoisin jvoisin 10334 Apr 28 22:20 leak_secrets.py
-rw-r--r--. 1 jvoisin jvoisin 9171 Apr 28 23:15 merge.py
-rw-r--r--. 1 jvoisin jvoisin 83861 Apr 27 23:59 NOTES.md