GHSA-h2x6-g7q6-344v Gitea: Repository migration SSRF via multi-answer DNS allow-list bypass
SummaryGitea's repository migration URL validation can be bypassed when a migration hostname resolves to multiple IP addresses. The validation logic accepts the destination if **any** resolved IP is allowed, even if another resolved IP is loopback, private, or otherwise blocked. The later `git clone` operation resolves the hostname again outside of that validation decision, so it can connect to the internal address.An authenticated low-privilege user who can create repository migrations can use an attacker-controlled DNS name to make Gitea connect to internal-only Git services and import their contents into a repository controlled by the attacker.DetailsThe issue is in `services/migrations/migrate.go`, in the migration allow/block-list check.Current logic computes whether any resolved IP is allowed:Then, when an allow-list is active, the host is accepted if the hostname matches or `ipAllowed` is true:This means a hostname resolving to both:an allowed public IP, e.g. `1.2.3.4`a blocked internal IP, e.g. `127.0.0.1`passes validation because the public IP sets `ipAllowed = true`.The actual repository import is later performed by `git clone --mirror` via `MigrateRepositoryGitData` / `gitrepo.CloneExternalRepo`. That git subprocess performs its own DNS resolution and is not tied to the specific IP set that was validated earlier. If the hostname resolves, rotates, or is re-bound to the internal address at clone time, Gitea can connect to a destination the migration filter would reject if supplied directly.The direct internal URL is correctly blocked, but the multi-answer hostname is accepted.PoCI verified the vulnerable predicate locally against Gitea checkout:using Dockerized Go tests with `golang:1.26.4`.The local test proves:`checkByAllowBlockList("loopback.example.test", [127.0.0.1])` is rejected.`checkByAllowBlockList("mixed.example.test", [1.2.3.4, 127.0.0.1])` is accepted.Minimal reproducer at the validation layer:To reproduce end-to-end:Run Gitea with repository migration enabled and `ALLOW_LOCALNETWORKS = false`.Create a normal non-admin user that can create repositories.Run an internal Git HTTP service reachable only from the Gitea server, for example on `127.0.0.1:18082`.Configure an attacker-controlled hostname so that DNS can return both a public address and `127.0.0.1`, or can return a public address during Gitea's pre-flight validation and `127.0.0.1` during the later git clone.Confirm the direct internal migration is rejected:Expected direct result:Start a migration from the attacker-controlled multi-answer hostname:Expected vulnerable result:The migration request is accepted.The git subprocess can connect to the internal address.Internal repository contents are imported into the attacker's new Gitea repository.ImpactThis is a server-side request forgery in repository migration.An authenticated user with permission to create repository migrations can make the Gitea server connect to internal-only network resources that are normally blocked by the migration SSRF filter. If the internal service is a Git repository or Git-compatible HTTP endpoint, its contents can be imported into an attacker-controlled repository and exfiltrated.Potentially impacted resources include:internal Git repositorieslocalhost-only servicesprivate network source-control servicesmetadata or internal infrastructure endpoints if reachable and compatible with the request pathThe direct internal destination is rejected, but a multi-answer or rebindable DNS name can pass validation and later resolve to the internal address during the clone operation.Suggested fixThe migration allow/block-list check should fail closed for multi-answer DNS:reject if **any** resolved IP is blockedrequire **all** resolved IPs to be allowed when an allow-list is activetreat an empty resolution result as not IP-allowedideally enforce the same destination policy at connection time, not only during pre-flight validation, to avoid DNS TOCTOU between validation and `git clone`For example, instead of `ipAllowed = ipAllowed || allowList.MatchIPAddr(addr)`, initialize `ipAllowed` to `len(addrList) > 0` and combine with logical AND:
CVSS v3.1 score: 6.5