Severity by source
AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N
AV:L because exploitation requires local CLI execution on the Gitea host; S:C because SSRF extends impact to internal network services and cloud metadata endpoints beyond the Gitea process.
Primary rating from Vendor (https://github.com/go-gitea/gitea).
CVSS VectorVendor: https://github.com/go-gitea/gitea
Lifecycle Timeline
2DescriptionCVE.org
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 remoteresulting 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
- Create a dump directory following the normal
restore-repolayout
(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"- Run
gitea restore-repoagainst that dump directory for any repo
owner.
- Observe on the target host/listener: an actual
gitHTTP
discovery request arrives, e.g. GET /ssrf-proof/info/refs?service=git-upload-pack, driven entirely by the value from the dump file.
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.
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.
AnalysisAI
Server-Side Request Forgery in Gitea's restore-repo CLI command allows an attacker who can supply a malicious dump archive to a Gitea administrator to force the server to issue git fetch against arbitrary attacker-controlled or internal URLs, including cloud metadata endpoints and local filesystem paths via file://. The root cause is a Go language logic defect: CheckAndEnsureSafePR is invoked with an empty commonCloneBaseURL string, causing strings.HasPrefix(anything, "") to always return true, which silently disables the URL validation guard for Head.CloneURL in the restore path only. A detailed PoC is publicly documented in the GHSA advisory; no CISA KEV listing is present, indicating active exploitation is unconfirmed at time of analysis. Fix is available in Gitea v1.27.0.
Technical ContextAI
The affected component is Gitea's repository migration/restore subsystem, specifically services/migrations/restore.go and services/migrations/common.go. When gitea restore-repo processes a dump archive, it unmarshals pull_request.yml into base.PullRequest structs and then calls CheckAndEnsureSafePR(pr, "", r) - passing an empty string as the commonCloneBaseURL parameter. The guard function hasBaseURL uses strings.HasPrefix(toCheck, baseURL) which in Go always evaluates to true when baseURL is an empty string, making the entire safety check a no-op for this call site. All other downloaders (github.go, gitlab.go, gitea_downloader.go, codebase.go, codecommit.go, onedev.go) pass a real base URL, so they are unaffected. The unvalidated Head.CloneURL value is then passed directly to g.gitRepo.AddRemote() and subsequently used in a live git fetch, triggering outbound network requests to attacker-specified destinations. CWE-918 (Server-Side Request Forgery) accurately describes the root cause class. The affected package is code.gitea.io/gitea (Go module). RCE via git's ext:: transport was tested and ruled out by the reporter, as standard git installations reject that transport by default. This is a distinct root cause from the previously patched path-traversal issue in #38215, which addressed DownloadURL/PatchURL but left Head.CloneURL unguarded.
RemediationAI
Upgrade Gitea to v1.27.0 or later, which resolves the missing URL validation in the restore-repo path. The release is available at https://github.com/go-gitea/gitea/releases/tag/v1.27.0. If immediate upgrade is not feasible, restrict execution of the gitea restore-repo CLI command to a minimal set of trusted operators and enforce that all dump archives originate from trusted, internally generated sources only - do not accept dump archives from external or untrusted parties. As a network-level compensating control, configure egress firewall rules on the Gitea host to block outbound connections to RFC-1918 address ranges, link-local addresses (169.254.0.0/16), and loopback, which limits the SSRF impact surface by preventing cloud metadata endpoint access; note this does not mitigate file://-based local repository disclosure. Auditing or pre-validating pull_request.yml files within dump archives before running restore-repo can also detect malicious head.cloneURL values, though this requires manual inspection. The advisory is GHSA-xmj7-xj85-hfc3.
Remote code execution in Gitea (self-hosted Git service) via a code-injection flaw (CWE-94) allows attackers to run arbi
Broken access control in Gitea's Composer package registry (versions up to and including 1.26.1) lets remote attackers r
Gitea before 1.16.7 does not escape git fetch remote. Rated high severity (CVSS 7.5), this vulnerability is remotely exp
The git hook feature in Gitea 1.1.0 through 1.12.5 might allow for authenticated remote code execution in customer envir
Improper Neutralization of Input During Web Page Generation (XSS or 'Cross-site Scripting') vulnerability in Gitea Gitea
Reverse-proxy authentication bypass in the official Gitea Docker image (versions up to and including 1.26.2) allows any
Container escape in Gitea act_runner (Docker backend, through act 0.262.0) lets an authenticated user with workflow-exec
models/repo_mirror.go in Gitea before 1.7.6 and 1.8.x before 1.8-RC3 mishandles mirror repo URL settings, leading to rem
Server-side request forgery and internal repository exfiltration in Gitea before 1.27.0 lets a low-privileged authentica
Authorization bypass in Gitea versions 1.22.3 through 1.26.1 allows holders of `public-only` access tokens or OAuth gran
An issue was discovered in Gitea through 1.11.5. Rated high severity (CVSS 7.5), this vulnerability is remotely exploita
Missing Authorization in GitHub repository go-gitea/gitea prior to 1.16.4. Rated high severity (CVSS 7.1), this vulnerab
Same weakness CWE-918 – Server-Side Request Forgery (SSRF)
View allVendor StatusVendor
SUSE
Severity: Moderate| Product | Status |
|---|---|
| SUSE Linux Enterprise Server 16.1 | Affected |
| SUSE Linux Enterprise Server for SAP applications 16.1 | Affected |
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-58168
GHSA-xmj7-xj85-hfc3