mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-09 19:00:02 +00:00
Backport #38318 by @bircni Draft-release access control was enforced only on the API release endpoints (`/api/v1/repos/{owner}/{repo}/releases/...`) but not on the UUID-based web attachment endpoints (`/attachments/{uuid}`, `/{owner}/{repo}/attachments/{uuid}`, `/{owner}/{repo}/releases/attachments/{uuid}`). Anyone who obtained an attachment UUID — including unauthenticated callers — could download files belonging to a hidden draft release, since `ServeAttachment` only checked repo-level read permission and never the release's draft state. This extends `ServeAttachment` to require write access to releases when the attachment belongs to a draft release, mirroring the existing API-side `canAccessReleaseDraft` gate. A regression test is included. Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
@@ -186,6 +186,22 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Draft release attachments must not be exposed to anyone without write
|
||||
// access, matching the API-side canAccessReleaseDraft gate. Otherwise the
|
||||
// UUID-based web endpoints would leak draft attachments to any recipient of
|
||||
// the (leaked) download URL.
|
||||
if unitType == unit.TypeReleases && attach.ReleaseID != 0 && !perm.CanWrite(unit.TypeReleases) {
|
||||
rel, err := repo_model.GetReleaseByID(ctx, attach.ReleaseID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetReleaseByID", err)
|
||||
return
|
||||
}
|
||||
if rel.IsDraft {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if requiredScope, ok := attachmentReadScope(unitType); ok {
|
||||
context.CheckTokenScopes(ctx, repo, requiredScope)
|
||||
if ctx.Written() {
|
||||
|
||||
@@ -171,6 +171,11 @@ func testGetAttachment(t *testing.T) {
|
||||
{"PrivateAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, user2Session, http.StatusOK},
|
||||
{"RepoNotAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12", true, user8Session, http.StatusNotFound},
|
||||
{"OrgNotAccessibleByUser", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a21", true, user8Session, http.StatusNotFound},
|
||||
// draft release attachments must only be reachable by users with write access, even on a public repo
|
||||
{"DraftReleaseByOwner", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a23", true, user2Session, http.StatusOK},
|
||||
{"DraftReleaseByAdmin", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a23", true, adminSession, http.StatusOK},
|
||||
{"DraftReleaseByNonCollaborator", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a23", true, user8Session, http.StatusNotFound},
|
||||
{"DraftReleaseByAnonymous", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a23", true, emptySession, http.StatusNotFound},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user