One header hands Gitea Docker admin to anyone on the network
You stood up Gitea in a docker compose file six months ago, told the team to push their side projects there, and stopped thinking about it. The container sits on the dev VPC, not the public internet, and the front page still reports version 1.26.2. That is the exact profile the scanner at 159.26.98[.]241 was hunting on July 7. One HTTP request from anywhere on the network, a single header attached, and it read every private repository on the box as gitea_admin.
CVE-2026-20896 is why, and NVD rated it 9.8 (SecurityWeek). The bug is not code that mishandles input. It is a shipped configuration where the Docker image told Gitea, in effect, that every source IP on earth was a trusted reverse proxy.
How Gitea learned to trust a header
Gitea supports a reverse-proxy authentication mode where nginx or Traefik negotiates SSO against your directory and hands Gitea the resolved username on the internal hop as an HTTP header. The knob is ENABLE_REVERSE_PROXY_AUTHENTICATION in app.ini, and the header Gitea trusts is X-WEBAUTH-USER. When a request lands with that header set, Gitea skips its own login flow and treats the string in the header as the authenticated user. If the string is admin, the request is admin.
The guardrail meant to keep that pattern safe is REVERSE_PROXY_TRUSTED_PROXIES, a list of source IP ranges Gitea will accept the header from. In principle you set that to the address of your nginx sidecar and nothing else.
Why the Docker image trusted the whole world
The official Gitea Docker image shipped app.ini with REVERSE_PROXY_TRUSTED_PROXIES = * as the default, meaning every source IP was treated as a legitimate reverse proxy (SC Media). That wildcard was a stand-in for "the operator will pick a real subnet later," and a lot of operators never did. Every fresh docker run gitea/gitea:1.26.2 came up believing every packet reaching its listener came from a trusted authenticating proxy. Anything on a network segment that could reach the port could send the header and be treated as whichever user it named.
Joshua Martinelle at Tenable and community researcher @rz1027 reported the configuration privately. The fix in PR #38151 stripped both REVERSE_PROXY_LIMIT and REVERSE_PROXY_TRUSTED_PROXIES from the shipped Docker template and made reverse-proxy authentication an explicit opt-in that only activates after an administrator sets both the flag and the trusted list by hand. That went out as 1.26.3; a regression required a follow-up, and 1.26.4 is the version to pin.
The whole exploit fits in a single curl
The unabbreviated request looks like this:
curl -H "X-WEBAUTH-USER: admin" http://gitea.internal:3000/
The response is the admin dashboard. From there the caller owns repository content, webhook definitions, and the personal access token surface, and if the instance is the source of truth for CI/CD, they own whatever runs when a commit lands on main. No rate limit helps, no MFA intercepts, no login page sits in the middle. The header is treated as identity, and the header comes from the attacker.
Two follow-ups make the primitive useful to an operator. First, mint a personal access token as admin so revoking the initial session does not evict them. Second, clone every private repository through the API, which honors the same header on every route.
Sysdig catches the first sweep from a ProtonVPN exit
The public advisory came out with 1.26.3 in mid-June. Sysdig's threat research team stood up honeypots mirroring a vulnerable 1.26.2 deployment and waited. The first hit landed thirteen days later, on July 7, from 159.26.98[.]241, an exit IP for the ProtonVPN service (The Hacker News). The traffic was reconnaissance: an automated scanner sweeping the internet for open Gitea listeners, testing the header, and logging the response. It never advanced to cloning against the honeypot, consistent with an operator still building a target list.
About 6,200 Gitea instances were reachable on the public internet during the sweep (Bleeping Computer). Internal instances behind corporate networks are not in that count, and those are typically where the source of truth for production repositories lives.
Where a CVSS 9.8 alone misses the fanout
CVSS 9.8 tells you the bug is severe. It does not tell you which of your Gitea instances a scanner can actually reach. A dev-network container behind a bastion has the same version string as an internet-exposed one a partner engineer opened up "just for the week." The version match flags both critical, and the segment topology is what separates a burning incident from a scheduled Monday upgrade.
The other misread is the shape of the compromise. Most CVE feeds report impact as "authentication bypass, remote." For a source-hosting service that phrase is a floor, not a ceiling. Once the caller holds the admin API surface, they hold every runner token wired into Actions, every deploy key, every webhook secret, and the commit history a developer might have absent-mindedly checked a secret into. A scanner that stops at "auth bypass" is scoring the entrance, not the room.
Four TRIS layers that decide the queue
TRIS is the Threat and Risk Intelligence Scoring engine inside CVEasy AI. Four layers decide where this finding lands in a triage queue.
Exploitation state. Sysdig captured live reconnaissance on July 7 from a ProtonVPN exit, and multiple outlets have reported the same probe pattern (Security Affairs). TRIS records that as automated in-the-wild scanning against real instances, which is short of confirmed abuse of your specific asset but pushes exploitation state to the top of the ACT band.
Network exposure of the specific instance. TRIS asks whether the container's HTTP port answers on the public internet, on a partner VPN subnet, or only on a locked-down internal segment behind a bastion. The same CVE lands top-band on a 1.26.2 container answering on 0.0.0.0:3000 from a public IP, and drops a full band on an instance whose port only responds to a small allowlist of engineering laptops.
Blast radius from the container. TRIS reads the outbound reachability: does the instance hold credentials for a private container registry, an SSH deploy key that pushes to production, a webhook on the release automation, a service account for the object store where build artifacts land. Every one of those raises the score, because admin on Gitea is only the first link in the chain when the target ships software.
Patch status honest to the sources. A running build of 1.26.4 or later closes the finding. 1.26.3 alone does not, because the regression documented in the Gitea release notes leaves the door partially open on certain reverse-proxy topologies (Gitea blog). TRIS marks it closed only when the running image reports 1.26.4 or later, or the config both disables ENABLE_REVERSE_PROXY_AUTHENTICATION and drops any wildcard from the trusted-proxies list.
Net effect: an internet-facing 1.26.2 whose runner token can reach a production Kubernetes cluster lands top-band ACT, and a purely internal 1.26.2 whose port only answers from a management jump host sits mid-band on the analyst's normal change queue.
What to do before the next sweep reaches you
Pull the image tag first. A quick docker inspect on the Gitea container will tell you the version and the running config. If the tag is 1.26.2 or earlier, upgrade straight to 1.26.4 today; do not stop at 1.26.3, because the acknowledged regression reopens the issue on certain topologies. Take the container down, pull gitea/gitea:1.26.4, bring it back up against the same volume.
If the upgrade cannot happen right now, edit app.ini in the container and either set REVERSE_PROXY_TRUSTED_PROXIES to only the specific IP of your reverse proxy (never a wildcard, never a wide CIDR), or set ENABLE_REVERSE_PROXY_AUTHENTICATION = false if you are not actually using proxy-based SSO. The setting is read at startup, so restart after the edit.
Hunt in access logs. Any HTTP request to the Gitea listener carrying X-WEBAUTH-USER from any IP other than your legitimate reverse proxy is a hit; outside a working SSO setup the header is unusual traffic, and a grep over a week of nginx or Traefik logs will surface the payload cleanly if the frontend forwards headers into its log format. Inside Gitea, review the admin audit log for user-creation events and personal access token creations that no one on the team owns.
Rotate the derivatives if anything hits. Deploy keys, webhook signing secrets, admin and service-account tokens, and any registry or cloud credentials the Actions runners carry are reachable once an attacker holds admin. Treat them as burned and regenerate before the next build inherits a stale credential.
Where CVEasy AI drops this into your inventory
CVEasy AI, the number one local-first CTEM platform, ingests the Gitea release notes, the Tenable disclosure credit, and the Sysdig probe telemetry as they land. TRIS scores CVE-2026-20896 against the actual configuration of every Gitea container in your inventory: image tag, ENABLE_REVERSE_PROXY_AUTHENTICATION state, REVERSE_PROXY_TRUSTED_PROXIES contents, the segment the listener answers on, and the outbound reachability admin on the instance buys downstream. The finding names the exposed containers, orders them by real exposure rather than raw CVSS, and hands over the log query to check each one for prior access. Inventory and audit exports never leave your hardware; the analysis runs on the box you own. A version match was always going to score both containers the same, and the layers underneath decide which one is answering to a scanner right now.
Sources: Gitea 1.26.3 and 1.26.4 release notes, Gitea PR #38151, The Hacker News, Bleeping Computer, SecurityWeek, Security Affairs, SC Media