Gitea: SSRF in restore-repo via unsanitized pull_request.yml Head.CloneURL
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
별도 긴급 패치 불필요 — 정기 시스템 업그레이드 주기에 맞춰 조치
CVSS 벡터 · 메트릭
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N상세 설명
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:
1err = yaml.Unmarshal(bs, &pulls) 2... 3for _, pr := range pulls { 4 if pr.PatchURL != "" { 5 pr.PatchURL = "file://" + util.FilePathJoinAbs(r.baseDir, pr.PatchURL) 6 } 7 CheckAndEnsureSafePR(pr, "", r) // <-- empty baseURL 8}CheckAndEnsureSafePR (services/migrations/common.go) is supposed to
reject Head.CloneURL/PatchURL values that don't share a common base
URL:
1func hasBaseURL(toCheck, baseURL string) bool { 2 if len(baseURL) > 0 && baseURL[len(baseURL)-1] != '/' { 3 baseURL += "/" 4 } 5 return strings.HasPrefix(toCheck, baseURL) 6} 7 8func CheckAndEnsureSafePR(pr *base.PullRequest, commonCloneBaseURL string, g base.Downloader) bool { 9 valid := true10 if pr.PatchURL != "" && !hasBaseURL(pr.PatchURL, commonCloneBaseURL) {11 pr.PatchURL = ""12 valid = false13 }14 if pr.Head.CloneURL != "" && !hasBaseURL(pr.Head.CloneURL, commonCloneBaseURL) {15 pr.Head.CloneURL = ""16 valid = false17 }18 return valid19}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:
1err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true) 2// ... 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 apull_request.ymlcontaining at least
one entry with:
1 - number: 1 2 head: 3 cloneURL: "http://<attacker-controlled-or-internal-host>:<port>/ssrf-proof" 4 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.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.