
Proof-of-concept and lab for an authenticated SSRF in Gitea's repo migrate endpoint, where git follows HTTP redirects into loopback git HTTP and leaks internal objects.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · gitea-git-redir-ssrf
Gitea 1.27.3 - Gitea
IsMigrateURLAllowed is a one-shot net.LookupIP on the URL you typed. Bytes then go through git clone --mirror. HandleGitCmdHTTPRedirection is supposed to stop git from following that hop. It does nothing. The comment is the finding: they left http.followRedirects=false off because GitLab 301s without .git. Pull-mirror fetch calls the same no-op.
A signed-in user can migrate from an allow-listed bait host and git follows the 302 into loopback git HTTP. Internal objects land in a repo they own.
| ID | no CVE yet |
| CWE | CWE-918 |
| CVSS | High: 7.7 CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N |
| Product | Gitea |
| Affected | through v1.27.3 (146cc3e) |
| Auth | authenticated (default open registration) |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1 only |
Register, migrate from an external-looking URL that 302s to http://127.0.0.1:.../internal.git. Direct migrate to loopback is denied. Redirect-followed clone is not. Witness file lands in their repo.
Not file:// disk read. Not X-Gitea-Internal-Auth on /api/internal (git will not set that header). Not unauthenticated RCE. Not metadata RCE unless you separately show git stored the body.
Same tag leftovers: keys IDOR, hostmatcher 0.0.0.0/8.
v1.27.3 closed the 2026 GHSA wave: CVE-2026-60004 (diffpatch hook RCE, CISA KEV), CVE-2026-59774 (unauth Org-mode #+INCLUDE), fork-PR Actions gates, reverse-proxy X-WEBAUTH-USER default. Stay on 1.27.3+ for those. I sat on that tag anyway.
CVE-2026-59765 fixed Go uri.Open. CVE-2026-22874 fixed Go reservedIPNets. Git's redirector is still a FIXME. Changelog "disable HTTP redirects on pull mirror sync" is not this function.
Lab puts Gitea on 8.8.4.2 so a bait host at 8.8.4.10 looks external to hostmatcher. Internal git HTTP binds loopback only on the Gitea netns (network_mode: service:gitea, port 9418). Direct migrate to 127.0.0.1 must fail. Redirect-followed clone must succeed. redir.py 302s /bait.git to http://127.0.0.1:9418/internal.git. Witness file WITNESS contains GHSA-GITEA-GIT-REDIR-SSRF.
Wrong turns already recorded: ALLOW_LOCALNETWORKS true (then the allow-list is gone); file://; X-Gitea-Internal-Auth; a reverse shell. Theatre. Direct loopback migrate denied is the control.
Client bugs look like product bugs. A helper that shadows http.client never sends a packet. .recv() on an HTTPResponse is not .read(). I mention that once because it cost time and looked, for a minute, like 1.27.3 had finished the job.
cd lab
./run.sh
Target only http://127.0.0.1:18103. ALLOW_LOCALNETWORKS=false.
negative-loopback status=422 You can not import from disallowed hosts
migrate-bait status=201 clone_addr=http://8.8.4.10/bait.git
fetch raw/WITNESS status=200 GHSA-GITEA-GIT-REDIR-SSRF
SUCCESS Gitea #2 git HTTP redirect SSRF
Do not treat submit-time DNS as the boundary. Force http.followRedirects=false (and normalize clone URLs to .git so GitLab still works), or proxy git HTTP through a dialer that re-runs hostmatcher on every hop.
HandleGitCmdHTTPRedirection · IsMigrateURLAllowedGNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.