GHSA-2r5c-gw76-rh3w Gitea: Incomplete SSRF Protection in Webhook and Migration Allow-list Default Filter
SummaryGitea's default SSRF allow-list ([`MatchBuiltinExternal`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/modules/hostmatcher/hostmatcher.go#L26-L27), used by both webhook delivery and repository migrations) relies on Go's standard library [`net.IP.IsPrivate()`](https://pkg.go.dev/net#IP.IsPrivate), which only covers RFC 1918 and RFC 4193. As a result, several IP ranges commonly used for cloud metadata services, internal networks, and IPv6 transition mechanisms are not blocked, allowing authenticated users to send HTTP requests to those destinations and read the responses via the webhook history UI.DetailsThe vulnerability lives in [`HostMatchList.checkIP`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/modules/hostmatcher/hostmatcher.go#L96-L114), specifically [line 103](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/modules/hostmatcher/hostmatcher.go#L103):`net.IP.IsPrivate()` recognises only:`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` (RFC 1918)`fc00::/7` (RFC 4193 IPv6 ULA)It does **not** recognise:| Range | Description |
|---|---|
| `100.64.0.0/10` | RFC 6598 Carrier-Grade NAT |
| `168.63.129.16/32` | Azure WireServer metadata endpoint |
| `172.32.0.0/11` | Non-RFC1918 portion of `172.0.0.0/8` (real-world internal use) |
| `64:ff9b::/96` | RFC 6052 IPv6 NAT64 (can embed `169.254.169.254`) |
| `2001::/32` | RFC 4380 Teredo tunneling |
| `2002::/16` | RFC 3056 6to4 |
| `2001:db8::/32` | RFC 3849 documentation |The default is reached by webhook delivery at [`services/webhook/deliver.go#L312-L316`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/services/webhook/deliver.go#L312-L316) and by repository migrations at [`services/migrations/migrate.go#L522`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/services/migrations/migrate.go#L522).The SSRF is **not blind**. Webhook delivery captures the response status, headers, and up to 1 MiB of body ([`services/webhook/deliver.go#L259-L270`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/services/webhook/deliver.go#L259-L270)) and renders them in the webhook history UI ([`templates/repo/settings/webhook/history.tmpl#L75-L85`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/templates/repo/settings/webhook/history.tmpl#L75-L85)), so attackers can read everything the targeted internal service returns.ImpactAn authenticated user who can create or modify a webhook can:Reach cloud metadata endpoints (AWS IMDS via NAT64 `64:ff9b::a9fe:a9fe`, Azure WireServer `168.63.129.16`)Probe and exfiltrate from internal services on CGNAT (`100.64.0.0/10`) or non-RFC1918 172.x rangesRead full HTTP response bodies through the webhook history UIThe same default is applied to repository migrations, broadening the attack surface to users who can trigger a migration.Proof of ConceptThe attached patch ([`gitea_ssrf_test.patch`](#patch)) adds [`TestSSRFBypassRanges`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/modules/hostmatcher/hostmatcher_test.go) to the existing test file. It uses [Go subtests](https://go.dev/blog/subtests) so each vulnerable range is its own named failing test case.Run with:Expected result on `4c37f4dacbac022f7beca75272439331f0368830`:**8 PASS** — RFC 1918, IPv6 private ranges, and legitimate public IPs (control cases)**10 FAIL** — Each failing subtest is a documented SSRF bypassSample failure output:Suggested RemediationI'd suggest treating the design of [`MatchBuiltinExternal`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/modules/hostmatcher/hostmatcher.go#L96-L114) as the bug — `IsPrivate()` is too narrow a definition of "internal." A comprehensive deny-list approach is what's needed here. For reference, CC-Tweaked's [`AddressPredicate.PrivatePattern`](https://github.com/cc-tweaked/CC-Tweaked/blob/3e7ce15ba6d5ab030092850f7e49829b64ba3555/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java#L116-L169) is a good reference list, blocking each of the ranges named in the table above plus a few others (multicast, broadcast, TEST-NET, etc.).The exact remediation is at your discretion.ReferencesVulnerable function: [`modules/hostmatcher/hostmatcher.go#L96-L114`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/modules/hostmatcher/hostmatcher.go#L96-L114)Default for webhooks: [`services/webhook/deliver.go#L312-L316`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/services/webhook/deliver.go#L312-L316)Default for migrations: [`services/migrations/migrate.go#L522`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/services/migrations/migrate.go#L522)Response captured: [`services/webhook/deliver.go#L259-L270`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/services/webhook/deliver.go#L259-L270)Response rendered: [`templates/repo/settings/webhook/history.tmpl#L75-L85`](https://github.com/go-gitea/gitea/blob/4c37f4dacbac022f7beca75272439331f0368830/templates/repo/settings/webhook/history.tmpl#L75-L85)Go stdlib `net.IP.IsPrivate()`: <https://pkg.go.dev/net#IP.IsPrivate>CC-Tweaked reference deny-list: [`AddressPredicate.java#L116-L169`](https://github.com/cc-tweaked/CC-Tweaked/blob/3e7ce15ba6d5ab030092850f7e49829b64ba3555/projects/core/src/main/java/dan200/computercraft/core/apis/http/options/AddressPredicate.java#L116-L169)Go subtests pattern: <https://go.dev/blog/subtests>RFC 6598 (CGNAT), RFC 6052 (NAT64), RFC 4380 (Teredo), RFC 3056 (6to4), RFC 3849 (documentation)Patch <a name="patch"></a>Proposed `gitea_ssrf_test.patch`:CreditThis vulnerability was uncovered by @JLLeitschuh of the @braze-inc security team.
CVSS v3.1 score: 9.6