Skip to content

Commit cb338a2

Browse files
GiteaBota1012112796wxiaoguang
authored
fix attachment file size limit in server backend (go-gitea#35519) (go-gitea#35720)
Backport go-gitea#35519 by @a1012112796 fix go-gitea#35512 Co-authored-by: a1012112796 <[email protected]> Co-authored-by: wxiaoguang <[email protected]>
1 parent 16f4f0d commit cb338a2

File tree

18 files changed

+172
-112
lines changed

18 files changed

+172
-112
lines changed

modules/git/utils.go

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package git
66
import (
77
"crypto/sha1"
88
"encoding/hex"
9-
"io"
109
"strconv"
1110
"strings"
1211
"sync"
@@ -68,32 +67,6 @@ func ParseBool(value string) (result, valid bool) {
6867
return intValue != 0, true
6968
}
7069

71-
// LimitedReaderCloser is a limited reader closer
72-
type LimitedReaderCloser struct {
73-
R io.Reader
74-
C io.Closer
75-
N int64
76-
}
77-
78-
// Read implements io.Reader
79-
func (l *LimitedReaderCloser) Read(p []byte) (n int, err error) {
80-
if l.N <= 0 {
81-
_ = l.C.Close()
82-
return 0, io.EOF
83-
}
84-
if int64(len(p)) > l.N {
85-
p = p[0:l.N]
86-
}
87-
n, err = l.R.Read(p)
88-
l.N -= int64(n)
89-
return n, err
90-
}
91-
92-
// Close implements io.Closer
93-
func (l *LimitedReaderCloser) Close() error {
94-
return l.C.Close()
95-
}
96-
9770
func HashFilePathForWebUI(s string) string {
9871
h := sha1.New()
9972
_, _ = h.Write([]byte(s))

modules/setting/attachment.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ var Attachment AttachmentSettingType
1616
func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
1717
Attachment = AttachmentSettingType{
1818
AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip",
19-
MaxSize: 2048,
20-
MaxFiles: 5,
21-
Enabled: true,
19+
20+
// FIXME: this size is used for both "issue attachment" and "release attachment"
21+
// The design is not right, these two should be different settings
22+
MaxSize: 2048,
23+
24+
MaxFiles: 5,
25+
Enabled: true,
2226
}
2327
sec, _ := rootCfg.GetSection("attachment")
2428
if sec == nil {

modules/util/error.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var (
1616
ErrPermissionDenied = errors.New("permission denied") // also implies HTTP 403
1717
ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404
1818
ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409
19+
ErrContentTooLarge = errors.New("content exceeds limit") // also implies HTTP 413
1920

2021
// ErrUnprocessableContent implies HTTP 422, the syntax of the request content is correct,
2122
// but the server is unable to process the contained instructions

routers/api/v1/repo/issue_attachment.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
package repo
55

66
import (
7+
"errors"
78
"net/http"
89

910
issues_model "code.gitea.io/gitea/models/issues"
1011
repo_model "code.gitea.io/gitea/models/repo"
1112
"code.gitea.io/gitea/modules/log"
1213
"code.gitea.io/gitea/modules/setting"
1314
api "code.gitea.io/gitea/modules/structs"
15+
"code.gitea.io/gitea/modules/util"
1416
"code.gitea.io/gitea/modules/web"
1517
attachment_service "code.gitea.io/gitea/services/attachment"
1618
"code.gitea.io/gitea/services/context"
@@ -154,6 +156,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
154156
// "$ref": "#/responses/error"
155157
// "404":
156158
// "$ref": "#/responses/error"
159+
// "413":
160+
// "$ref": "#/responses/error"
157161
// "422":
158162
// "$ref": "#/responses/validationError"
159163
// "423":
@@ -181,7 +185,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
181185
filename = query
182186
}
183187

184-
attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
188+
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
189+
attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
185190
Name: filename,
186191
UploaderID: ctx.Doer.ID,
187192
RepoID: ctx.Repo.Repository.ID,
@@ -190,6 +195,8 @@ func CreateIssueAttachment(ctx *context.APIContext) {
190195
if err != nil {
191196
if upload.IsErrFileTypeForbidden(err) {
192197
ctx.APIError(http.StatusUnprocessableEntity, err)
198+
} else if errors.Is(err, util.ErrContentTooLarge) {
199+
ctx.APIError(http.StatusRequestEntityTooLarge, err)
193200
} else {
194201
ctx.APIErrorInternal(err)
195202
}

routers/api/v1/repo/issue_comment_attachment.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"code.gitea.io/gitea/modules/log"
1414
"code.gitea.io/gitea/modules/setting"
1515
api "code.gitea.io/gitea/modules/structs"
16+
"code.gitea.io/gitea/modules/util"
1617
"code.gitea.io/gitea/modules/web"
1718
attachment_service "code.gitea.io/gitea/services/attachment"
1819
"code.gitea.io/gitea/services/context"
@@ -161,6 +162,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
161162
// "$ref": "#/responses/forbidden"
162163
// "404":
163164
// "$ref": "#/responses/error"
165+
// "413":
166+
// "$ref": "#/responses/error"
164167
// "422":
165168
// "$ref": "#/responses/validationError"
166169
// "423":
@@ -189,7 +192,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
189192
filename = query
190193
}
191194

192-
attachment, err := attachment_service.UploadAttachment(ctx, file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
195+
uploaderFile := attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
196+
attachment, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Attachment.AllowedTypes, &repo_model.Attachment{
193197
Name: filename,
194198
UploaderID: ctx.Doer.ID,
195199
RepoID: ctx.Repo.Repository.ID,
@@ -199,6 +203,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
199203
if err != nil {
200204
if upload.IsErrFileTypeForbidden(err) {
201205
ctx.APIError(http.StatusUnprocessableEntity, err)
206+
} else if errors.Is(err, util.ErrContentTooLarge) {
207+
ctx.APIError(http.StatusRequestEntityTooLarge, err)
202208
} else {
203209
ctx.APIErrorInternal(err)
204210
}

routers/api/v1/repo/release_attachment.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
package repo
55

66
import (
7-
"io"
7+
"errors"
88
"net/http"
99
"strings"
1010

1111
repo_model "code.gitea.io/gitea/models/repo"
1212
"code.gitea.io/gitea/modules/log"
1313
"code.gitea.io/gitea/modules/setting"
1414
api "code.gitea.io/gitea/modules/structs"
15+
"code.gitea.io/gitea/modules/util"
1516
"code.gitea.io/gitea/modules/web"
1617
attachment_service "code.gitea.io/gitea/services/attachment"
1718
"code.gitea.io/gitea/services/context"
@@ -191,6 +192,8 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
191192
// "$ref": "#/responses/error"
192193
// "404":
193194
// "$ref": "#/responses/notFound"
195+
// "413":
196+
// "$ref": "#/responses/error"
194197

195198
// Check if attachments are enabled
196199
if !setting.Attachment.Enabled {
@@ -205,10 +208,8 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
205208
}
206209

207210
// Get uploaded file from request
208-
var content io.ReadCloser
209211
var filename string
210-
var size int64 = -1
211-
212+
var uploaderFile *attachment_service.UploaderFile
212213
if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
213214
file, header, err := ctx.Req.FormFile("attachment")
214215
if err != nil {
@@ -217,15 +218,14 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
217218
}
218219
defer file.Close()
219220

220-
content = file
221-
size = header.Size
222221
filename = header.Filename
223222
if name := ctx.FormString("name"); name != "" {
224223
filename = name
225224
}
225+
uploaderFile = attachment_service.NewLimitedUploaderKnownSize(file, header.Size)
226226
} else {
227-
content = ctx.Req.Body
228227
filename = ctx.FormString("name")
228+
uploaderFile = attachment_service.NewLimitedUploaderMaxBytesReader(ctx.Req.Body, ctx.Resp)
229229
}
230230

231231
if filename == "" {
@@ -234,7 +234,7 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
234234
}
235235

236236
// Create a new attachment and save the file
237-
attach, err := attachment_service.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
237+
attach, err := attachment_service.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, setting.Repository.Release.AllowedTypes, &repo_model.Attachment{
238238
Name: filename,
239239
UploaderID: ctx.Doer.ID,
240240
RepoID: ctx.Repo.Repository.ID,
@@ -245,6 +245,12 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
245245
ctx.APIError(http.StatusBadRequest, err)
246246
return
247247
}
248+
249+
if errors.Is(err, util.ErrContentTooLarge) {
250+
ctx.APIError(http.StatusRequestEntityTooLarge, err)
251+
return
252+
}
253+
248254
ctx.APIErrorInternal(err)
249255
return
250256
}

routers/web/repo/attachment.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
4545
}
4646
defer file.Close()
4747

48-
attach, err := attachment.UploadAttachment(ctx, file, allowedTypes, header.Size, &repo_model.Attachment{
48+
uploaderFile := attachment.NewLimitedUploaderKnownSize(file, header.Size)
49+
attach, err := attachment.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, allowedTypes, &repo_model.Attachment{
4950
Name: header.Filename,
5051
UploaderID: ctx.Doer.ID,
5152
RepoID: repoID,

routers/web/repo/editor_uploader.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func UploadFileToServer(ctx *context.Context) {
4141
return
4242
}
4343

44+
// FIXME: need to check the file size according to setting.Repository.Upload.FileMaxSize
45+
4446
uploaded, err := repo_model.NewUpload(ctx, name, buf, file)
4547
if err != nil {
4648
ctx.ServerError("NewUpload", err)

services/attachment/attachment.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ package attachment
66
import (
77
"bytes"
88
"context"
9+
"errors"
910
"fmt"
1011
"io"
12+
"net/http"
1113

1214
"code.gitea.io/gitea/models/db"
1315
repo_model "code.gitea.io/gitea/models/repo"
16+
"code.gitea.io/gitea/modules/setting"
1417
"code.gitea.io/gitea/modules/storage"
1518
"code.gitea.io/gitea/modules/util"
1619
"code.gitea.io/gitea/services/context/upload"
@@ -28,27 +31,56 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
2831
attach.UUID = uuid.New().String()
2932
size, err := storage.Attachments.Save(attach.RelativePath(), file, size)
3033
if err != nil {
31-
return fmt.Errorf("Create: %w", err)
34+
return fmt.Errorf("Attachments.Save: %w", err)
3235
}
3336
attach.Size = size
34-
3537
return db.Insert(ctx, attach)
3638
})
3739

3840
return attach, err
3941
}
4042

41-
// UploadAttachment upload new attachment into storage and update database
42-
func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
43+
type UploaderFile struct {
44+
rd io.ReadCloser
45+
size int64
46+
respWriter http.ResponseWriter
47+
}
48+
49+
func NewLimitedUploaderKnownSize(r io.Reader, size int64) *UploaderFile {
50+
return &UploaderFile{rd: io.NopCloser(r), size: size}
51+
}
52+
53+
func NewLimitedUploaderMaxBytesReader(r io.ReadCloser, w http.ResponseWriter) *UploaderFile {
54+
return &UploaderFile{rd: r, size: -1, respWriter: w}
55+
}
56+
57+
func UploadAttachmentGeneralSizeLimit(ctx context.Context, file *UploaderFile, allowedTypes string, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
58+
return uploadAttachment(ctx, file, allowedTypes, setting.Attachment.MaxSize<<20, attach)
59+
}
60+
61+
func uploadAttachment(ctx context.Context, file *UploaderFile, allowedTypes string, maxFileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
62+
src := file.rd
63+
if file.size < 0 {
64+
src = http.MaxBytesReader(file.respWriter, src, maxFileSize)
65+
}
4366
buf := make([]byte, 1024)
44-
n, _ := util.ReadAtMost(file, buf)
67+
n, _ := util.ReadAtMost(src, buf)
4568
buf = buf[:n]
4669

4770
if err := upload.Verify(buf, attach.Name, allowedTypes); err != nil {
4871
return nil, err
4972
}
5073

51-
return NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), file), fileSize)
74+
if maxFileSize >= 0 && file.size > maxFileSize {
75+
return nil, util.ErrorWrap(util.ErrContentTooLarge, "attachment exceeds limit %d", maxFileSize)
76+
}
77+
78+
attach, err := NewAttachment(ctx, attach, io.MultiReader(bytes.NewReader(buf), src), file.size)
79+
var maxBytesError *http.MaxBytesError
80+
if errors.As(err, &maxBytesError) {
81+
return nil, util.ErrorWrap(util.ErrContentTooLarge, "attachment exceeds limit %d", maxFileSize)
82+
}
83+
return attach, err
5284
}
5385

5486
// UpdateAttachment updates an attachment, verifying that its name is among the allowed types.

services/context/api.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,7 @@ func APIContexter() func(http.Handler) http.Handler {
229229

230230
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
231231
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
232-
if err := ctx.Req.ParseMultipartForm(setting.Attachment.MaxSize << 20); err != nil && !strings.Contains(err.Error(), "EOF") { // 32MB max size
233-
ctx.APIErrorInternal(err)
232+
if !ctx.ParseMultipartForm() {
234233
return
235234
}
236235
}

0 commit comments

Comments
 (0)