Browse Source
Attachments: Add extension support, allow all types for releases (#12465)
Attachments: Add extension support, allow all types for releases (#12465)
* Attachments: Add extension support, allow all types for releases - Add support for file extensions, matching the `accept` attribute of `<input type="file">` - Add support for type wildcard mime types, e.g. `image/*` - Create repository.release.ALLOWED_TYPES setting (default unrestricted) - Change default for attachment.ALLOWED_TYPES to a list of extensions - Split out POST /attachments into two endpoints for issue/pr and releases to prevent circumvention of allowed types check Fixes: https://github.com/go-gitea/gitea/pull/10172 Fixes: https://github.com/go-gitea/gitea/issues/7266 Fixes: https://github.com/go-gitea/gitea/pull/12460 Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers * rename function * extract GET routes out of RepoMustNotBeArchived Co-authored-by: Lauris BH <lauris@nix.lv>mj-v1.14.3
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 497 additions and 226 deletions
-
13custom/conf/app.example.ini
-
19docs/content/doc/advanced/config-cheat-sheet.en-us.md
-
2integrations/attachment_test.go
-
51models/twofactor.go
-
68modules/secret/secret.go
-
13modules/secret/secret_test.go
-
3modules/setting/attachment.go
-
16modules/setting/repository.go
-
46modules/upload/filetype.go
-
47modules/upload/filetype_test.go
-
94modules/upload/upload.go
-
195modules/upload/upload_test.go
-
3routers/api/v1/repo/release_attachment.go
-
21routers/repo/attachment.go
-
4routers/repo/compare.go
-
26routers/repo/editor.go
-
12routers/repo/issue.go
-
4routers/repo/pull.go
-
7routers/repo/release.go
-
19routers/routes/routes.go
-
2templates/repo/editor/upload.tmpl
-
8templates/repo/issue/comment_tab.tmpl
-
15templates/repo/issue/view_content.tmpl
-
8templates/repo/release/new.tmpl
-
13templates/repo/upload.tmpl
-
14web_src/js/index.js
@ -1,46 +0,0 @@ |
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|||
// Use of this source code is governed by a MIT-style
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package upload |
|||
|
|||
import ( |
|||
"fmt" |
|||
"net/http" |
|||
"strings" |
|||
|
|||
"code.gitea.io/gitea/modules/log" |
|||
) |
|||
|
|||
// ErrFileTypeForbidden not allowed file type error
|
|||
type ErrFileTypeForbidden struct { |
|||
Type string |
|||
} |
|||
|
|||
// IsErrFileTypeForbidden checks if an error is a ErrFileTypeForbidden.
|
|||
func IsErrFileTypeForbidden(err error) bool { |
|||
_, ok := err.(ErrFileTypeForbidden) |
|||
return ok |
|||
} |
|||
|
|||
func (err ErrFileTypeForbidden) Error() string { |
|||
return fmt.Sprintf("File type is not allowed: %s", err.Type) |
|||
} |
|||
|
|||
// VerifyAllowedContentType validates a file is allowed to be uploaded.
|
|||
func VerifyAllowedContentType(buf []byte, allowedTypes []string) error { |
|||
fileType := http.DetectContentType(buf) |
|||
|
|||
for _, t := range allowedTypes { |
|||
t := strings.Trim(t, " ") |
|||
|
|||
if t == "*/*" || t == fileType || |
|||
// Allow directives after type, like 'text/plain; charset=utf-8'
|
|||
strings.HasPrefix(fileType, t+";") { |
|||
return nil |
|||
} |
|||
} |
|||
|
|||
log.Info("Attachment with type %s blocked from upload", fileType) |
|||
return ErrFileTypeForbidden{Type: fileType} |
|||
} |
@ -1,47 +0,0 @@ |
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|||
// Use of this source code is governed by a MIT-style
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package upload |
|||
|
|||
import ( |
|||
"bytes" |
|||
"compress/gzip" |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
func TestUpload(t *testing.T) { |
|||
testContent := []byte(`This is a plain text file.`) |
|||
var b bytes.Buffer |
|||
w := gzip.NewWriter(&b) |
|||
w.Write(testContent) |
|||
w.Close() |
|||
|
|||
kases := []struct { |
|||
data []byte |
|||
allowedTypes []string |
|||
err error |
|||
}{ |
|||
{ |
|||
data: testContent, |
|||
allowedTypes: []string{"text/plain"}, |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
allowedTypes: []string{"application/x-gzip"}, |
|||
err: ErrFileTypeForbidden{"text/plain; charset=utf-8"}, |
|||
}, |
|||
{ |
|||
data: b.Bytes(), |
|||
allowedTypes: []string{"application/x-gzip"}, |
|||
err: nil, |
|||
}, |
|||
} |
|||
|
|||
for _, kase := range kases { |
|||
assert.Equal(t, kase.err, VerifyAllowedContentType(kase.data, kase.allowedTypes)) |
|||
} |
|||
} |
@ -0,0 +1,94 @@ |
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|||
// Use of this source code is governed by a MIT-style
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package upload |
|||
|
|||
import ( |
|||
"net/http" |
|||
"path" |
|||
"regexp" |
|||
"strings" |
|||
|
|||
"code.gitea.io/gitea/modules/context" |
|||
"code.gitea.io/gitea/modules/log" |
|||
"code.gitea.io/gitea/modules/setting" |
|||
) |
|||
|
|||
// ErrFileTypeForbidden not allowed file type error
|
|||
type ErrFileTypeForbidden struct { |
|||
Type string |
|||
} |
|||
|
|||
// IsErrFileTypeForbidden checks if an error is a ErrFileTypeForbidden.
|
|||
func IsErrFileTypeForbidden(err error) bool { |
|||
_, ok := err.(ErrFileTypeForbidden) |
|||
return ok |
|||
} |
|||
|
|||
func (err ErrFileTypeForbidden) Error() string { |
|||
return "This file extension or type is not allowed to be uploaded." |
|||
} |
|||
|
|||
var mimeTypeSuffixRe = regexp.MustCompile(`;.*$`) |
|||
var wildcardTypeRe = regexp.MustCompile(`^[a-z]+/\*$`) |
|||
|
|||
// Verify validates whether a file is allowed to be uploaded.
|
|||
func Verify(buf []byte, fileName string, allowedTypesStr string) error { |
|||
allowedTypesStr = strings.ReplaceAll(allowedTypesStr, "|", ",") // compat for old config format
|
|||
|
|||
allowedTypes := []string{} |
|||
for _, entry := range strings.Split(allowedTypesStr, ",") { |
|||
entry = strings.ToLower(strings.TrimSpace(entry)) |
|||
if entry != "" { |
|||
allowedTypes = append(allowedTypes, entry) |
|||
} |
|||
} |
|||
|
|||
if len(allowedTypes) == 0 { |
|||
return nil // everything is allowed
|
|||
} |
|||
|
|||
fullMimeType := http.DetectContentType(buf) |
|||
mimeType := strings.TrimSpace(mimeTypeSuffixRe.ReplaceAllString(fullMimeType, "")) |
|||
extension := strings.ToLower(path.Ext(fileName)) |
|||
|
|||
// 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 { |
|||
return nil // extension is allowed
|
|||
} else if mimeType == allowEntry { |
|||
return nil // mime type is allowed
|
|||
} else 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) |
|||
return ErrFileTypeForbidden{Type: fullMimeType} |
|||
} |
|||
|
|||
// AddUploadContext renders template values for dropzone
|
|||
func AddUploadContext(ctx *context.Context, uploadType string) { |
|||
if uploadType == "release" { |
|||
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/releases/attachments" |
|||
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/releases/attachments/remove" |
|||
ctx.Data["UploadAccepts"] = strings.Replace(setting.Repository.Release.AllowedTypes, "|", ",", -1) |
|||
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles |
|||
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize |
|||
} else if uploadType == "comment" { |
|||
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments" |
|||
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove" |
|||
ctx.Data["UploadAccepts"] = strings.Replace(setting.Attachment.AllowedTypes, "|", ",", -1) |
|||
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles |
|||
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize |
|||
} else if uploadType == "repo" { |
|||
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file" |
|||
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove" |
|||
ctx.Data["UploadAccepts"] = strings.Replace(setting.Repository.Upload.AllowedTypes, "|", ",", -1) |
|||
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles |
|||
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize |
|||
} |
|||
} |
@ -0,0 +1,195 @@ |
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|||
// Use of this source code is governed by a MIT-style
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package upload |
|||
|
|||
import ( |
|||
"bytes" |
|||
"compress/gzip" |
|||
"testing" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
) |
|||
|
|||
func TestUpload(t *testing.T) { |
|||
testContent := []byte(`This is a plain text file.`) |
|||
var b bytes.Buffer |
|||
w := gzip.NewWriter(&b) |
|||
w.Write(testContent) |
|||
w.Close() |
|||
|
|||
kases := []struct { |
|||
data []byte |
|||
fileName string |
|||
allowedTypes string |
|||
err error |
|||
}{ |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "dir/test.txt", |
|||
allowedTypes: "", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "../../../test.txt", |
|||
allowedTypes: "", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: ",", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "|", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "*/*", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "*/*,", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "*/*|", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "text/plain", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "dir/test.txt", |
|||
allowedTypes: "text/plain", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "/dir.txt/test.js", |
|||
allowedTypes: ".js", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: " text/plain ", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: ".txt", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: " .txt,.js", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: " .txt|.js", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "../../test.txt", |
|||
allowedTypes: " .txt|.js", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: " .txt ,.js ", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "text/plain, .txt", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "text/*", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "text/*,.js", |
|||
err: nil, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "text/**", |
|||
err: ErrFileTypeForbidden{"text/plain; charset=utf-8"}, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: "application/x-gzip", |
|||
err: ErrFileTypeForbidden{"text/plain; charset=utf-8"}, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: ".zip", |
|||
err: ErrFileTypeForbidden{"text/plain; charset=utf-8"}, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: ".zip,.txtx", |
|||
err: ErrFileTypeForbidden{"text/plain; charset=utf-8"}, |
|||
}, |
|||
{ |
|||
data: testContent, |
|||
fileName: "test.txt", |
|||
allowedTypes: ".zip|.txtx", |
|||
err: ErrFileTypeForbidden{"text/plain; charset=utf-8"}, |
|||
}, |
|||
{ |
|||
data: b.Bytes(), |
|||
fileName: "test.txt", |
|||
allowedTypes: "application/x-gzip", |
|||
err: nil, |
|||
}, |
|||
} |
|||
|
|||
for _, kase := range kases { |
|||
assert.Equal(t, kase.err, Verify(kase.data, kase.fileName, kase.allowedTypes)) |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
<div |
|||
class="ui dropzone" |
|||
id="dropzone" |
|||
data-upload-url="{{.UploadUrl}}" |
|||
data-remove-url="{{.UploadRemoveUrl}}" |
|||
data-accepts="{{.UploadAccepts}}" |
|||
data-max-file="{{.UploadMaxFiles}}" |
|||
data-max-size="{{.UploadMaxSize}}" |
|||
data-default-message="{{.i18n.Tr "dropzone.default_message"}}" |
|||
data-invalid-input-type="{{.i18n.Tr "dropzone.invalid_input_type"}}" |
|||
data-file-too-big="{{.i18n.Tr "dropzone.file_too_big"}}" |
|||
data-remove-file="{{.i18n.Tr "dropzone.remove_file"}}" |
|||
></div> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue