Skip to main content
VulnerabilityPublished Updated

CVE-2026-58314: Gitea · Two SSRF findings

Affects code.gitea.io/giteaSource GCVE Database

0
Compare
Authoritative record:CVE-2026-58314· High 7.7

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

Data as of 30 Oct 2026, 11:52 UTC

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

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

| --- | --- | | Versions tested | `gitea/gitea:1.26.2` (digest `sha256:7d13848af12645600a5f9d93ee2560daa9c6fa6b5b859b7bff3a5e1c0b661031`); `gitea/gitea:latest` resolves to the same digest at time of writing | | Source review | `git checkout v1.26.2` (commit `2c749ce`) | | Reproduction | `bash run_poc.sh` (single shot: brings up containers, runs three PoCs, prints captured evidence, tears down on exit) | | Files touched by the fixes | `modules/hostmatcher/hostmatcher.go`, `modules/auth/openid/openid.go` | ## Summary Gitea guards outbound HTTP from webhooks and repo migration with `net.Dialer.Control`, the correct hook point. The IP classifier behind it misses ten address families, of which CGNAT (100.64.0.0/10) is the practically important one because it is plain IPv4 and is used today by Tailscale, AWS VPC secondary CIDRs, and several Kubernetes pod-CIDR conventions. Any logged-in user can create a webhook pointing at an internal CGNAT host. The full HTTP response from that host (status, headers, body up to 1 MB) is stored in the webhook delivery log and rendered to the webhook owner on the hook detail page. The same gap applies to repo migration. Separately, the OpenID sign-in form at `/user/login/openid` fetches the user-supplied provider URL server-side via `openid-go`, which uses `http.DefaultClient`. No `hostmatcher`, no IP filter, no CSRF, no authentication. When OpenID sign-in is enabled, anyone on the internet can drive Gitea into making arbitrary GET requests against internal IPs. Both reproduce on `gitea/gitea:1.26.2` (current stable) in default configuration. The bundled `run_poc.sh` reproduces all three primitives end-to-end in about one minute and tears the lab down at exit. --- ## Finding 1: hostmatcher classifier passes CGNAT and IPv6 transition prefixes ### The bug `modules/hostmatcher/hostmatcher.go:107-119`, the `external` builtin: ```go case MatchBuiltinExternal: if ip.IsGlobalUnicast() && !ip.IsPrivate() { return true } ``` `IsGlobalUnicast() && !IsPrivate()` was written for stack bookkeeping, not as a security boundary. It catches RFC 1918, RFC 4193 ULA, link-local, loopback, and the IPv4-mapped `::ffff:0:0/96` prefix (because `IsPrivate` un-embeds that range via `net.IP.To4()`). Every other IPv6 transition prefix returns nil from `To4()`, so the embedded IPv4 is invisible to the classifier. Address families that pass the guard, verified live on 1.26.2: | Family | Example | Why missed | | --- | --- | --- | | CGNAT, RFC 6598 | `100.64.0.1` | `IsPrivate()` checks 10/8, 172.16/12, 192.168/16 only | | NAT64 well-known, RFC 6052 | `64:ff9b::7f00:1` | `To4()` un-embeds only `::ffff:0:0/96` | | NAT64 local-use, RFC 8215 | `64:ff9b:1::1` | same | | 6to4, RFC 3056 | `2002:7f00:1::` | same | | Teredo, RFC 4380 | `2001::abcd` | same | | IPv4-compatible | `::169.254.169.254` | same | | SIIT, RFC 6145 | `::ffff:0:7f00:1` | same | | Documentation | `2001:db8::1` | same | | Benchmarking, RFC 2544 | `198.18.0.1` | not in any private check | | TEST-NET, RFC 5737 | `192.0.2.1` | same | CGNAT is the one that needs no IPv6 infrastructure. The 100.64.0.0/10 block overlaps with Tailscale (100.x), AWS VPC secondary CIDRs, and several Kubernetes pod-CIDR conventions. Any Gitea instance that shares a network with services on a CGNAT address is exploitable in default config. NAT64, 6to4, Teredo, and SIIT need the matching gateway on the network to actually carry the packet. The guard still passes the address, so the bug is real at the guard layer; impact depends on whether the deployment has the corresponding transition mechanism. Lab confirmed: the guard passes, the connection then fails with `network is unreachable` on a stock Linux box. The wrapping `net.Dialer.Control` callback is structurally correct (fires per redirect hop, post-DNS, pre-connect). The classifier is the only broken part. ### Reachable sinks Webhook delivery, `services/webhook/deliver.go`: ```go // :321-328 webhookHTTPClient = &http.Client{ Timeout: timeout, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, Proxy: webhookProxy(allowedHostMatcher), DialContext: hostmatcher.NewDialContext("webhook", allowedHostMatcher, nil, setting.Webhook.ProxyURLFixed), }, } ``` Authentication required: any logged-in user via user-level hooks (`POST /api/v1/user/hooks`) or repo-admin via repo-level hooks. The response body is captured up to 1 MB: ```go // :268 p, err := util.ReadWithLimit(resp.Body, 1024*1024) ``` and stored in `hook_task.response_content`, then rendered on `/{owner}/{repo}/settings/hooks/{id}`. Repo migration, `services/migrations/http_client.go:27`: ```go DialContext: hostmatcher.NewDialContext("migration", allowList, blockList, setting.Proxy.ProxyURLFixed), ``` with a pre-flight check at `services/migrations/migrate.go:43-87` (`IsMigrateURLAllowed`). Both layers call the same `MatchIPAddr`, s

References

Community reviews

Loading reviews…