Skip to main content
VulnerabilityPublished

CVE-2026-54481: Gitea · Internal API HTTP client hardcodes InsecureSkipVerify:true with no config override

Affects code.gitea.io/giteaSource GCVE Database

0
Compare
Authoritative record:CVE-2026-54481· High 7.5

Not known to be exploited · CVSS 7.5 High : upgrade to 1.27.0 soon.

Data as of 30 Oct 2026, 11:52 UTC

CVSS 3.1 base score
7.5High
How severe the flaw is on the 0–10 CVSS scale.
EPSS
0.3% chance of exploitation in 30 days – higher than 22% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
Patch
Available – fixed in 1.27.0
CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

Exploitation evidence

t = 0 is the CVE’s publication on cve.org

  1. Disclosed: evidence found.

    CVE published 13 Aug 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponized: evidence found.

    CISA SSVC: proof of concept public

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

Timeline

  1. CVE ID reserved

    by Gitea Limited

    , 36 days before disclosure

  2. CVE published

    on cve.org

    , t = 0

  3. CISA SSVC assessment

    proof of concept public

    , 25 hours after disclosure

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), CISA SSVC, the Metasploit, Exploit-DB and nuclei-templates index as of 30 Oct 2026.

What to do

Upgrade code.gitea.io/gitea to 1.27.0.

Affected software

ProductAffectedFixed in
Gocode.gitea.io/giteaAffected
  • <1.27.0
Fixed in
  • 1.27.0

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

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://<host>/ 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/SMTP SkipVerify; incoming-mail SkipTLSVerify). The internal API client alone is hardcoded insecure with no opt-out. The InsecureSkipVerify line has been present since 2017 (#1471), so all releases are affected. PoC Goal: capture the live INTERNAL_TOKEN from a real Gitea subprocess call and replay it. Note: a self-contained TLS test (e.g. Python ssl.CERT_NONE accepting a self-signed cert) only restates the flag's definition and does NOT involve Gitea. The steps below exercise the real path. 1. Configure Gitea so the internal client uses HTTPS to an interceptable target: [server] PROTOCOL = https LOCAL_ROOT_URL = https://127.0.0.1:8443/ 2. Run a rogue TLS listener on 127.0.0.1:8443 presenting ANY self-signed certificate, logging the X-Gitea-Internal-Auth request header. Minimal handler: # python3 rogue.py import http.server, ssl, subprocess subprocess.run(["openssl","req","-x509","-newkey","rsa:2048","-keyout","k.pem","-out","c.pem",

References

Community reviews

Loading reviews…