"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/attachment"
+ attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
filename = query
}
- attachment, err := attachment.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
+ attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
// "$ref": "#/responses/Attachment"
// "404":
// "$ref": "#/responses/error"
+ // "422":
+ // "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
attachment.Name = form.Name
}
- if err := repo_model.UpdateAttachment(ctx, attachment); err != nil {
+ if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attachment); err != nil {
+ if upload.IsErrFileTypeForbidden(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ return
+ }
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err)
+ return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attachment))
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/attachment"
+ attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
filename = query
}
- attachment, err := attachment.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
+ attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
// "$ref": "#/responses/Attachment"
// "404":
// "$ref": "#/responses/error"
+ // "422":
+ // "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
attach := getIssueCommentAttachmentSafeWrite(ctx)
attach.Name = form.Name
}
- if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
+ if err := attachment_service.UpdateAttachment(ctx, setting.Attachment.AllowedTypes, attach); err != nil {
+ if upload.IsErrFileTypeForbidden(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ return
+ }
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
+ return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/attachment"
+ attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/convert"
}
// Create a new attachment and save the file
- attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
+ attach, err := attachment_service.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
// responses:
// "201":
// "$ref": "#/responses/Attachment"
+ // "422":
+ // "$ref": "#/responses/validationError"
// "404":
// "$ref": "#/responses/notFound"
attach.Name = form.Name
}
- if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
+ if err := attachment_service.UpdateAttachment(ctx, setting.Repository.Release.AllowedTypes, attach); err != nil {
+ if upload.IsErrFileTypeForbidden(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ return
+ }
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
+ return
}
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
}
return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize)
}
+
+// UpdateAttachment updates an attachment, verifying that its name is among the allowed types.
+func UpdateAttachment(ctx context.Context, allowedTypes string, attach *repo_model.Attachment) error {
+ if err := upload.Verify(nil, attach.Name, allowedTypes); err != nil {
+ return err
+ }
+
+ return repo_model.UpdateAttachment(ctx, attach)
+}
}
func (err ErrFileTypeForbidden) Error() string {
- return "This file extension or type is not allowed to be uploaded."
+ return "This file cannot be uploaded or modified due to a forbidden file extension or type."
}
var wildcardTypeRe = regexp.MustCompile(`^[a-z]+/\*$`)
-// Verify validates whether a file is allowed to be uploaded.
+// Verify validates whether a file is allowed to be uploaded. If buf is empty, it will just check if the file
+// has an allowed file extension.
func Verify(buf []byte, fileName, allowedTypesStr string) error {
allowedTypesStr = strings.ReplaceAll(allowedTypesStr, "|", ",") // compat for old config format
return ErrFileTypeForbidden{Type: fullMimeType}
}
extension := strings.ToLower(path.Ext(fileName))
+ isBufEmpty := len(buf) <= 1
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers
for _, allowEntry := range allowedTypes {
if allowEntry == "*/*" {
return nil // everything allowed
- } else if strings.HasPrefix(allowEntry, ".") && allowEntry == extension {
+ }
+ if strings.HasPrefix(allowEntry, ".") && allowEntry == extension {
return nil // extension is allowed
- } else if mimeType == allowEntry {
+ }
+ if isBufEmpty {
+ continue // skip mime type checks if buffer is empty
+ }
+ if mimeType == allowEntry {
return nil // mime type is allowed
- } else if wildcardTypeRe.MatchString(allowEntry) && strings.HasPrefix(mimeType, allowEntry[:len(allowEntry)-1]) {
+ }
+ if wildcardTypeRe.MatchString(allowEntry) && strings.HasPrefix(mimeType, allowEntry[:len(allowEntry)-1]) {
return nil // wildcard match, e.g. image/*
}
}
- log.Info("Attachment with type %s blocked from upload", fullMimeType)
+ if !isBufEmpty {
+ log.Info("Attachment with type %s blocked from upload", fullMimeType)
+ }
+
return ErrFileTypeForbidden{Type: fullMimeType}
}
"404": {
"$ref": "#/responses/error"
},
+ "422": {
+ "$ref": "#/responses/validationError"
+ },
"423": {
"$ref": "#/responses/repoArchivedError"
}
"404": {
"$ref": "#/responses/error"
},
+ "422": {
+ "$ref": "#/responses/validationError"
+ },
"423": {
"$ref": "#/responses/repoArchivedError"
}
},
"404": {
"$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
}
}
}
func TestAPIEditCommentAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
- const newAttachmentName = "newAttachmentName"
+ const newAttachmentName = "newAttachmentName.txt"
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6})
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID})
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID, Name: apiAttachment.Name})
}
+func TestAPIEditCommentAttachmentWithUnallowedFile(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6})
+ comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+
+ filename := "file.bad"
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d",
+ repoOwner.Name, repo.Name, comment.ID, attachment.ID)
+ req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+ "name": filename,
+ }).AddTokenAuth(token)
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+}
+
func TestAPIDeleteCommentAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func TestAPIEditIssueAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
- const newAttachmentName = "newAttachmentName"
+ const newAttachmentName = "hello_world.txt"
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID, Name: apiAttachment.Name})
}
+func TestAPIEditIssueAttachmentWithUnallowedFile(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: attachment.IssueID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
+
+ filename := "file.bad"
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, issue.Index, attachment.ID)
+ req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+ "name": filename,
+ }).AddTokenAuth(token)
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+}
+
func TestAPIDeleteIssueAttachment(t *testing.T) {
defer tests.PrepareTestEnv(t)()
--- /dev/null
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+)
+
+func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
+ // Limit the allowed release types (since by default there is no restriction)
+ defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".exe")()
+ defer tests.PrepareTestEnv(t)()
+
+ attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9})
+ release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
+ repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+
+ session := loginUser(t, repoOwner.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ filename := "file.bad"
+ urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", repoOwner.Name, repo.Name, release.ID, attachment.ID)
+ req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
+ "name": filename,
+ }).AddTokenAuth(token)
+
+ session.MakeRequest(t, req, http.StatusUnprocessableEntity)
+}