Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/util/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
ErrPermissionDenied = errors.New("permission denied") // also implies HTTP 403
ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404
ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409
ErrContentTooLarge = errors.New("content exceeds limit") // also implies HTTP 413

// ErrUnprocessableContent implies HTTP 422, the syntax of the request content is correct,
// but the server is unable to process the contained instructions
Expand Down
8 changes: 7 additions & 1 deletion routers/api/v1/repo/issue_attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
package repo

import (
"errors"
"net/http"

issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
Expand Down Expand Up @@ -154,6 +156,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
Expand Down Expand Up @@ -181,7 +185,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
filename = query
}

attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, setting.Attachment.MaxSize<<20, header.Size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
Expand All @@ -190,6 +194,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
} else {
ctx.APIErrorInternal(err)
}
Expand Down
7 changes: 6 additions & 1 deletion routers/api/v1/repo/issue_comment_attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
Expand Down Expand Up @@ -161,6 +162,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/error"
// "413":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
// "423":
Expand Down Expand Up @@ -189,7 +192,7 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
filename = query
}

attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, setting.Attachment.MaxSize<<20, header.Size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
Expand All @@ -199,6 +202,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
} else {
ctx.APIErrorInternal(err)
}
Expand Down
12 changes: 11 additions & 1 deletion routers/api/v1/repo/release_attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package repo

import (
"errors"
"io"
"net/http"
"strings"
Expand All @@ -12,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
attachment_service "code.gitea.io/gitea/services/attachment"
"code.gitea.io/gitea/services/context"
Expand Down Expand Up @@ -191,6 +193,8 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "413":
// "$ref": "#/responses/error"

// Check if attachments are enabled
if !setting.Attachment.Enabled {
Expand Down Expand Up @@ -234,7 +238,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
}

// Create a new attachment and save the file
attach, err := attachment_service.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
attach, err := attachment_service.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, setting.Attachment.MaxSize<<20, size, &repo_model.Attachment{
Name: filename,
UploaderID: ctx.Doer.ID,
RepoID: ctx.Repo.Repository.ID,
Expand All @@ -245,6 +249,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
ctx.APIError(http.StatusBadRequest, err)
return
}

if errors.Is(err, util.ErrContentTooLarge) {
ctx.APIError(http.StatusRequestEntityTooLarge, err)
return
}

ctx.APIErrorInternal(err)
return
}
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
}
defer file.Close()

attach, err := attachment.UploadAttachment(ctx, file, allowedTypes, header.Size, &repo_model.Attachment{
attach, err := attachment.UploadAttachment(ctx, file, allowedTypes, setting.Attachment.MaxSize<<20, header.Size, &repo_model.Attachment{
Name: header.Filename,
UploaderID: ctx.Doer.ID,
RepoID: repoID,
Expand Down
46 changes: 41 additions & 5 deletions services/attachment/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
return nil, fmt.Errorf("attachment %s should belong to a repository", attach.Name)
}

err := db.WithTx(ctx, func(ctx context.Context) error {
if err := db.WithTx(ctx, func(ctx context.Context) error {
attach.UUID = uuid.New().String()
size, err := storage.Attachments.Save(attach.RelativePath(), file, size)
if err != nil {
Expand All @@ -33,13 +33,36 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
attach.Size = size

return db.Insert(ctx, attach)
})
}); err != nil {
return nil, err
}

return attach, nil
}

return attach, err
type ErrAttachmentSizeExceed struct {
MaxSize int64
Size int64
}

func (e *ErrAttachmentSizeExceed) Error() string {
if e.Size == 0 {
return fmt.Sprintf("attachment size exceeds limit %d", e.MaxSize)
}
return fmt.Sprintf("attachment size %d exceeds limit %d", e.Size, e.MaxSize)
}

func (e *ErrAttachmentSizeExceed) Unwrap() error {
return util.ErrContentTooLarge
}

func (e *ErrAttachmentSizeExceed) Is(target error) bool {
_, ok := target.(*ErrAttachmentSizeExceed)
return ok
}

// UploadAttachment upload new attachment into storage and update database
func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, maxFileSize, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(file, buf)
buf = buf[:n]
Expand All @@ -48,7 +71,20 @@ func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string,
return nil, err
}

return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize)
reader := io.MultiReader(bytes.NewReader(buf), file)

// enforce file size limit
if maxFileSize >= 0 {
if fileSize > maxFileSize {
return nil, &ErrAttachmentSizeExceed{MaxSize: maxFileSize, Size: fileSize}
}
// limit reader to max file size with additional 1k more,
// to allow side-cases where encoding tells us its exactly maxFileSize but the actual created file is bit more,
// while still make sure the limit is enforced
reader = attachmentLimitedReader(reader, maxFileSize+1024)
}

return NewAttachment(ctx, attach, reader, fileSize)
}

// UpdateAttachment updates an attachment, verifying that its name is among the allowed types.
Expand Down
54 changes: 50 additions & 4 deletions services/attachment/attachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,77 @@ import (
_ "code.gitea.io/gitea/models/actions"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMain(m *testing.M) {
unittest.MainTest(m)
}

func TestUploadAttachment(t *testing.T) {
func TestNewAttachment(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})

fPath := "./attachment_test.go"
f, err := os.Open(fPath)
assert.NoError(t, err)
require.NoError(t, err)
defer f.Close()
fs, err := f.Stat()
require.NoError(t, err)

attach, err := NewAttachment(t.Context(), &repo_model.Attachment{
RepoID: 1,
UploaderID: user.ID,
Name: filepath.Base(fPath),
}, f, -1)
}, f, fs.Size())
assert.NoError(t, err)
assert.Equal(t, fs.Size(), attach.Size)

attachment, err := repo_model.GetAttachmentByUUID(t.Context(), attach.UUID)
assert.NoError(t, err)
assert.Equal(t, user.ID, attachment.UploaderID)
assert.Equal(t, int64(0), attachment.DownloadCount)
}

func TestUploadAttachment(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})

fPath := "./attachment_test.go"
f, err := os.Open(fPath)
require.NoError(t, err)
defer f.Close()
fs, err := f.Stat()
require.NoError(t, err)

t.Run("size to big", func(t *testing.T) {
attach, err := UploadAttachment(t.Context(), f, "", 10, fs.Size(), &repo_model.Attachment{
RepoID: 1,
UploaderID: user.ID,
Name: filepath.Base(fPath),
})
assert.ErrorIs(t, err, &ErrAttachmentSizeExceed{})
assert.Nil(t, attach)
})

t.Run("size was lied about", func(t *testing.T) {
attach, err := UploadAttachment(t.Context(), f, "", 10, 10, &repo_model.Attachment{
RepoID: 1,
UploaderID: user.ID,
Name: filepath.Base(fPath),
})
assert.ErrorIs(t, err, &ErrAttachmentSizeExceed{})
assert.Nil(t, attach)
})

t.Run("size was correct", func(t *testing.T) {
attach, err := UploadAttachment(t.Context(), f, "", fs.Size(), fs.Size(), &repo_model.Attachment{
RepoID: 1,
UploaderID: user.ID,
Name: filepath.Base(fPath),
})
assert.NoError(t, err)
require.NotNil(t, attach)
assert.Equal(t, user.ID, attach.UploaderID)
})
}
35 changes: 35 additions & 0 deletions services/attachment/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package attachment

import "io"

// modified version of io.LimitReader: https://cs.opensource.google/go/go/+/refs/tags/go1.25.1:src/io/io.go;l=458-482

// attachmentLimitedReader returns a Reader that reads from r
// but errors with ErrAttachmentSizeExceed after n bytes.
// The underlying implementation is a *attachmentReader.
func attachmentLimitedReader(r io.Reader, n int64) io.Reader { return &attachmentReader{r, n} }

// A attachmentReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns ErrAttachmentSizeExceed when N <= 0.
// Underlying errors are passed through.
type attachmentReader struct {
R io.Reader // underlying reader
N int64 // max bytes remaining
}

func (l *attachmentReader) Read(p []byte) (n int, err error) {
if l.N <= 0 {
return 0, &ErrAttachmentSizeExceed{MaxSize: l.N}
}
if int64(len(p)) > l.N {
p = p[0:l.N]
}
n, err = l.R.Read(p)
l.N -= int64(n)
return

Check failure on line 34 in services/attachment/reader.go

View workflow job for this annotation

GitHub Actions / lint-backend

bare-return: avoid using bare returns, please add return expressions (revive)

Check failure on line 34 in services/attachment/reader.go

View workflow job for this annotation

GitHub Actions / lint-go-windows

bare-return: avoid using bare returns, please add return expressions (revive)

Check failure on line 34 in services/attachment/reader.go

View workflow job for this annotation

GitHub Actions / lint-go-gogit

bare-return: avoid using bare returns, please add return expressions (revive)
}
8 changes: 7 additions & 1 deletion services/mailer/incoming/incoming_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package incoming
import (
"bytes"
"context"
"errors"
"fmt"

issues_model "code.gitea.io/gitea/models/issues"
Expand Down Expand Up @@ -85,7 +86,7 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u
attachmentIDs := make([]string, 0, len(content.Attachments))
if setting.Attachment.Enabled {
for _, attachment := range content.Attachments {
a, err := attachment_service.UploadAttachment(ctx, bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, int64(len(attachment.Content)), &repo_model.Attachment{
a, err := attachment_service.UploadAttachment(ctx, bytes.NewReader(attachment.Content), setting.Attachment.AllowedTypes, setting.Attachment.MaxSize<<20, int64(len(attachment.Content)), &repo_model.Attachment{
Name: attachment.Name,
UploaderID: doer.ID,
RepoID: issue.Repo.ID,
Expand All @@ -95,6 +96,11 @@ func (h *ReplyHandler) Handle(ctx context.Context, content *MailContent, doer *u
log.Info("Skipping disallowed attachment type: %s", attachment.Name)
continue
}
if errors.Is(err, util.ErrContentTooLarge) {
log.Info("Skipping attachment exceeding size limit: %s", attachment.Name)
continue
}

return err
}
attachmentIDs = append(attachmentIDs, a.UUID)
Expand Down
9 changes: 9 additions & 0 deletions templates/swagger/v1_json.tmpl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading