CVE-2026-54481
Gitea: Internal API HTTP client hardcodes InsecureSkipVerify:true with no config override
Summary
Summary Gitea's internal API HTTP client (modules/private/internal.go) hardcodes TLSClientConfig.InsecureSkipVerify = true with no configuration override. It is the only outbound TLS client in the codebase that cannot be made to verify its peer's certificate — webhook, migrations, MinIO, LDAP, SMTP, Redis and incoming-mail all expose a secure-by-default SkipVerify toggle, this one does not. When an operator configures internal communication over HTTPS to a non-loopback target (LOCAL_ROOT_URL=https:// / in a split-host / multi-pod topology), the gitea serv / gitea hook subprocess that calls the internal API will accept ANY TLS certificate. An attacker with on-path position on that internal segment can MITM the connection and capture the static high-privilege INTERNAL_TOKEN, which is the sole authentication for every /api/internal/* endpoint (server shutdown/restart, SSH key authorization, git command execution, repo hooks, mail send, runner-token generation). Severity is deployment-dependent: High for split-host HTTPS deployments; Low/Informational for the default single-host / unix-socket / HTTP-loopback deployment, where the call is loopback and not interceptable without local access (which already exposes the token directly). This report is rated for the affected configuration and the underlying defense-in-depth defect. Details Affected component: modules/private/internal.go The internal API transport hardcodes certificate-verification bypass: var internalAPITransport = sync.OnceValue(func() http.RoundTripper { return &http.Transport{ DialContext: dialContextInternalAPI, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, // hardcoded, no config gate ServerName: setting.Domain, // SNI only; NOT used for validation while skip=true }, } }) Because InsecureSkipVerify is true, ServerName is used only for SNI and any certificate (any CN, self-signed) is accepted; there is no accidental safety net. Verified exploit chain (read against main @ aab9737651, 2026-06-13): 1. TLS path is reached whenever LOCAL_ROOT_URL scheme is https — http.Transport applies TLSClientConfig only for https requests. (modules/private/internal.go:56-64) 2. The dialer connects to the real host from the URL, with no loopback pinning: dialContextInternalAPI -> d.DialContext(ctx, network, address), where address is the host:port from LOCAL_ROOT_URL. (modules/private/internal.go:37-54) 3. The client runs as a SEPARATE process, so a real socket is used and can cross hosts: - Built-in and external SSH both exec "gitea serv key-N" (modules/ssh/ssh.go:109,123; models/asymkey/ssh_key_authorized_keys.go via authorized_keys command=). - Git hooks exec "gitea hook ...". These subprocesses call back to LOCAL_ROOT_URL. Same host => loopback; split host => network hop. 4. INTERNAL_TOKEN is sent on every internal request as a static bearer header: Header("X-Gitea-Internal-Auth", "Bearer "+setting.InternalToken) (modules/private/internal.go:80) 5. A captured token is accepted and is the SOLE gate for all internal routes: authInternal() does subtle.ConstantTimeCompare(header, setting.InternalToken) and nothing else. (routers/private/internal.go:24-42). The server code even comments: "// TODO: use something like JWT or HMAC to avoid passing the token in the clear" (routers/private/internal.go:32) 6. Amplifier: internal routes are mounted on the main public listener, not a loopback-only socket: r.Mount("/api/internal", private.Routes()) (routers/init.go:185) so a stolen token is replayable by anyone who can reach the Gitea HTTP port. Inconsistency / root cause: every other outbound TLS client is configurable and secure-by-default (services/webhook/deliver.go Webhook.SkipTLSVerify; services/migrations/http_client.go Migrations.SkipTLSVerify; modules/storage/minio.go MINIO_INSECURE_SKIP_VERIFY; LDAP/
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| Go | code.gitea.io/gitea | — | 1.27.0 |
Remediation: Upgrade to 1.27.0 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.
CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.