Gitea's Docker Default Trusts Every Client: CVE-2026-20896 Ships Admin Access Through X-WEBAUTH-USER
Every so often a bug lands on our desk that reads more like a configuration story than a vulnerability story, and the Gitea Docker auth bypass is one of them. There is no memory corruption. There is no chain of clever gadgets. The official container image shipped with a single line in its default app.ini that told Gitea to trust every source IP on the internet as if it were a reverse proxy, and once that line was in place the rest of the exploit is a documented feature working exactly the way the docs say it should work.
We at BlueTeamAutomation kept coming back to CVE-2026-20896 because of who runs Gitea. It is the self-hosted Git people install when they have opinions about where their code lives. Startups keep their private repositories in it. Engineering teams pipe deploy keys and CI tokens through it. Air-gapped environments and internal developer platforms lean on it precisely because the whole point is that nothing leaves the network. This bug turned that promise inside out. Sysdig confirmed the first probes in the wild on July 7, thirteen days after the advisory shipped, coming through a ProtonVPN egress at 159.26.98.241, and Shodan puts roughly 6,200 Gitea instances on the public internet right now (SecurityWeek on Sysdig's telemetry, The Hacker News).
The wildcard in the app.ini
Gitea has a feature called reverse-proxy authentication. The idea predates most modern SSO layers and is straightforward. If you sit Gitea behind Authentik, oauth2-proxy, Traefik forward auth, or an nginx module that has already authenticated the user, the proxy sets X-WEBAUTH-USER: alice on the request it forwards, and Gitea takes that as a truthful assertion about who is making the call. No password, no MFA prompt, no OIDC token. The user identity travels as an HTTP header.
The setting that keeps this safe is REVERSE_PROXY_TRUSTED_PROXIES. Native Gitea installs default it to 127.0.0.0/8,::1/128, which restricts the trust to a proxy running on the same host. The official Docker image, however, shipped an app.ini template that hard-coded REVERSE_PROXY_TRUSTED_PROXIES = *, per Gitea's own 1.26.3 and 1.26.4 release notes. A wildcard here means every IP is a trusted proxy, so the "is this coming from my authenticating front door" check evaluates to true no matter where the request originates. Combine that with header-based reverse-proxy auth being available in the container by default and the whole authentication story collapses into a single HTTP request.
What X-WEBAUTH-USER actually does inside Gitea
When Gitea receives an HTTP request with reverse-proxy auth active, its middleware runs before any of the normal session logic. Gitea reads the source IP of the connection, looks it up against REVERSE_PROXY_TRUSTED_PROXIES, and, if the address is in the trusted list, treats whatever value is in X-WEBAUTH-USER as the logged-in user for that request. When the account exists locally it becomes the session identity for the call; when it does not and auto provisioning is on, Gitea creates it.
With the wildcard in place, an attacker who can reach the container's HTTP port sends something like:
GET /-/admin HTTP/1.1
Host: gitea.example.com
X-WEBAUTH-USER: admin
Accept: text/html
If admin is a valid account, that request lands in the admin panel. If your admin is named after the person who set the box up, guessing it is trivial. If the instance auto provisions, the attacker picks any string, gets a fresh account back, and works from there. There is no password check, no rate limit against valid credentials because no credentials were ever supplied, and no anomalous login source because reverse-proxy auth is not expected to have one. As SC Media summarized, one header is the entire chain.
Once inside, the blast radius is whatever that user could see: private repositories, CI configuration, deploy keys, personal access tokens, SSH keys, and any secret a developer accidentally committed. Landing as admin adds the ability to attach new SSH keys to any repository, mint fresh personal access tokens, and read every piece of code on the instance (BleepingComputer coverage).
The exploit request, and what the scanner did with it
Sysdig's honeypot picked up the first in-the-wild attempt from 159.26.98.241, a ProtonVPN egress. The activity read as automated reconnaissance: HTTPS fingerprinting to confirm the target was a Gitea instance and then the auth bypass probe with common admin usernames. It had not yet progressed to repository theft or backdoored deploy keys at the time of Sysdig's report (The Hacker News, SecurityWeek). We treat that as the beginning of a wave, not the end of one, because a public PoC and detection tool landed on GitHub on July 3, and probing usually widens once tooling is commoditized.
Why your SOC never sees this coming
If you rely on failed login rate limits, brute force detections, or credential stuffing signals to notice unauthorized access, this bug is invisible to you. There is no failed login. There is no credential submission. The request is a single valid HTTP GET or POST that happens to carry an extra header, and WAFs that only inspect the request path and body pass it through without objection because X-WEBAUTH-USER is a legitimate header for internal reverse-proxy flows that normally never appears from external clients.
Repository clone activity looks normal too. The audit log entry, if you are collecting it, records a successful action from a real account. The user agent might be git/2.x, curl, or a browser. Unless your SIEM correlates that account's activity against expected geographies or ties Gitea session events back to your identity provider, the theft looks exactly like the developer working from a coffee shop. That is the shape of the detection gap: the observable is a header appearing where a header should not appear, and almost nothing in a stock blue-team stack is looking for that.
What to hunt for right now
The observable footprint is small but distinctive, and it maps cleanly onto a few concrete queries you can run in your SIEM and EDR today:
- Any HTTP request reaching your Gitea container carrying
X-WEBAUTH-USERfrom a source that is not the reverse proxy you actually operate. Enable request-header logging at your reverse proxy or ingress and search forX-WEBAUTH-USERoriginating from IPs outside your proxy fleet. - Gitea audit log entries where the authentication source is
ReverseProxyfor accounts that historically log in via password, OIDC, or LDAP. Drift from a user's normal login mechanism is the tell. - New personal access tokens, new SSH keys attached to accounts, and new deploy keys attached to repositories inside a window that covers late June and July, especially for accounts that did not touch CI during that window.
- Bulk clone activity from a single session, requests to
/-/admin/*from accounts that have not previously touched admin routes, and any repository export API calls following an unusual login. - Traffic to the container's HTTP port from any source other than your reverse proxy. If the container is meant to sit behind a proxy, everything else is by definition suspect.
Map the intrusion to ATT&CK as T1190 for initial access via the exposed application, T1078 for the resulting valid-account use, T1552.001 for the secrets those accounts hold in .env files and CI config, and T1213.003 for the code repository theft that typically follows.
Fix, mitigate, rotate
Upgrade the official Docker image to Gitea 1.26.4. Skip 1.26.3; that release removed the wildcard default but shipped a regression that breaks repository code page rendering, and 1.26.4 addresses both the original bug and the regression (Gitea 1.26.3 and 1.26.4 release notes).
If you cannot upgrade this week, set REVERSE_PROXY_TRUSTED_PROXIES to the specific IP or CIDR of your actual proxy in app.ini, or set ENABLE_REVERSE_PROXY_AUTHENTICATION = false if you are not using header-based SSO at all. Confirm the container's HTTP port is not directly reachable from anywhere except the proxy; a good default is that the container binds to a docker-internal network only, with the proxy as its sole ingress. Then walk the exposure: rotate every deploy key on affected repositories, every personal access token on affected accounts, every SSH key attached to admin accounts, and any secret that has ever been committed to a repository on that instance. When you cannot rule out exfiltration, assume the code is gone and revoke like it is.
Verifying the fix is not "does the container run": it is a probe from an untrusted network with an X-WEBAUTH-USER header, confirming the response is a 401 or a login page and not the account panel. That negative case is what actually proves the wildcard is closed.
Continuous proof, not a scramble
The reason we care about a config default is that CVE-2026-20896 lives on the identity plane, where our detection engineers spend most of their time and where a stock SOC's tooling has the least coverage. The BTA loop continuously replays the exact traffic in your environment, watches your EDR and SIEM either flag it or miss it, and closes the loop with SOAR-driven response and rolling compliance evidence, so the answer to "would we catch this next Tuesday" stops being a scramble every time a new CVE lands.
Prove your Gitea detection actually works.
BASzy replays the CVE-2026-20896 auth bypass against your environment, verifies your SOC saw it, and generates the compliance evidence automatically.
Talk to BlueTeamAutomation →