Skip to content

Commit cda4475

Browse files
silverwindlafriks
andauthored
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: #10172 Fixes: #7266 Fixes: #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 <[email protected]>
1 parent 67a5573 commit cda4475

26 files changed

+497
-226
lines changed

custom/conf/app.example.ini

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ LOCAL_COPY_PATH = tmp/local-repo
8888
ENABLED = true
8989
; Path for uploads. Defaults to `data/tmp/uploads` (tmp gets deleted on gitea restart)
9090
TEMP_PATH = data/tmp/uploads
91-
; One or more allowed types, e.g. image/jpeg|image/png. Nothing means any file type
91+
; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
9292
ALLOWED_TYPES =
9393
; Max size of each file in megabytes. Defaults to 3MB
9494
FILE_MAX_SIZE = 3
@@ -117,6 +117,10 @@ DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY=true
117117
; List of reasons why a Pull Request or Issue can be locked
118118
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam
119119

120+
[repository.release]
121+
; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
122+
ALLOWED_TYPES =
123+
120124
[repository.signing]
121125
; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
122126
; run in the context of the RUN_USER
@@ -766,11 +770,10 @@ DISABLE_GRAVATAR = false
766770
ENABLE_FEDERATED_AVATAR = false
767771

768772
[attachment]
769-
; Whether attachments are enabled. Defaults to `true`
773+
; Whether issue and pull request attachments are enabled. Defaults to `true`
770774
ENABLED = true
771-
772-
; One or more allowed types, e.g. "image/jpeg|image/png". Use "*/*" for all types.
773-
ALLOWED_TYPES = image/jpeg|image/png|application/zip|application/gzip
775+
; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
776+
ALLOWED_TYPES = .docx,.gif,.gz,.jpeg,.jpg,.log,.pdf,.png,.pptx,.txt,.xlsx,.zip
774777
; Max size of each file. Defaults to 4MB
775778
MAX_SIZE = 4
776779
; Max number of files per upload. Defaults to 5

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,18 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
101101

102102
- `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked
103103

104+
### Repository - Upload (`repository.upload`)
105+
106+
- `ENABLED`: **true**: Whether repository file uploads are enabled
107+
- `TEMP_PATH`: **data/tmp/uploads**: Path for uploads (tmp gets deleted on gitea restart)
108+
- `ALLOWED_TYPES`: **\<empty\>**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
109+
- `FILE_MAX_SIZE`: **3**: Max size of each file in megabytes.
110+
- `MAX_FILES`: **5**: Max number of files per upload
111+
112+
### Repository - Release (`repository.release`)
113+
114+
- `ALLOWED_TYPES`: **\<empty\>**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
115+
104116
### Repository - Signing (`repository.signing`)
105117

106118
- `SIGNING_KEY`: **default**: \[none, KEYID, default \]: Key to sign with.
@@ -560,11 +572,10 @@ Default templates for project boards:
560572
- `PROJECT_BOARD_BASIC_KANBAN_TYPE`: **To Do, In Progress, Done**
561573
- `PROJECT_BOARD_BUG_TRIAGE_TYPE`: **Needs Triage, High Priority, Low Priority, Closed**
562574

563-
## Attachment (`attachment`)
575+
## Issue and pull request attachments (`attachment`)
564576

565-
- `ENABLED`: **true**: Enable this to allow uploading attachments.
566-
- `ALLOWED_TYPES`: **see app.example.ini**: Allowed MIME types, e.g. `image/jpeg|image/png`.
567-
Use `*/*` for all types.
577+
- `ENABLED`: **true**: Whether issue and pull request attachments are enabled.
578+
- `ALLOWED_TYPES`: **.docx,.gif,.gz,.jpeg,.jpg,.log,.pdf,.png,.pptx,.txt,.xlsx,.zip**: Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
568579
- `MAX_SIZE`: **4**: Maximum size (MB).
569580
- `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
570581
- `STORAGE_TYPE`: **local**: Storage type for attachments, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`

integrations/attachment_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func createAttachment(t *testing.T, session *TestSession, repoURL, filename stri
4343

4444
csrf := GetCSRF(t, session, repoURL)
4545

46-
req := NewRequestWithBody(t, "POST", "/attachments", body)
46+
req := NewRequestWithBody(t, "POST", repoURL+"/issues/attachments", body)
4747
req.Header.Add("X-Csrf-Token", csrf)
4848
req.Header.Add("Content-Type", writer.FormDataContentType())
4949
resp := session.MakeRequest(t, req, expectedStatus)

models/twofactor.go

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,14 @@
55
package models
66

77
import (
8-
"crypto/aes"
9-
"crypto/cipher"
108
"crypto/md5"
11-
"crypto/rand"
129
"crypto/sha256"
1310
"crypto/subtle"
1411
"encoding/base64"
15-
"errors"
1612
"fmt"
17-
"io"
1813

1914
"code.gitea.io/gitea/modules/generate"
15+
"code.gitea.io/gitea/modules/secret"
2016
"code.gitea.io/gitea/modules/setting"
2117
"code.gitea.io/gitea/modules/timeutil"
2218

@@ -67,8 +63,8 @@ func (t *TwoFactor) getEncryptionKey() []byte {
6763
}
6864

6965
// SetSecret sets the 2FA secret.
70-
func (t *TwoFactor) SetSecret(secret string) error {
71-
secretBytes, err := aesEncrypt(t.getEncryptionKey(), []byte(secret))
66+
func (t *TwoFactor) SetSecret(secretString string) error {
67+
secretBytes, err := secret.AesEncrypt(t.getEncryptionKey(), []byte(secretString))
7268
if err != nil {
7369
return err
7470
}
@@ -82,51 +78,14 @@ func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
8278
if err != nil {
8379
return false, err
8480
}
85-
secret, err := aesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
81+
secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
8682
if err != nil {
8783
return false, err
8884
}
89-
secretStr := string(secret)
85+
secretStr := string(secretBytes)
9086
return totp.Validate(passcode, secretStr), nil
9187
}
9288

93-
// aesEncrypt encrypts text and given key with AES.
94-
func aesEncrypt(key, text []byte) ([]byte, error) {
95-
block, err := aes.NewCipher(key)
96-
if err != nil {
97-
return nil, err
98-
}
99-
b := base64.StdEncoding.EncodeToString(text)
100-
ciphertext := make([]byte, aes.BlockSize+len(b))
101-
iv := ciphertext[:aes.BlockSize]
102-
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
103-
return nil, err
104-
}
105-
cfb := cipher.NewCFBEncrypter(block, iv)
106-
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
107-
return ciphertext, nil
108-
}
109-
110-
// aesDecrypt decrypts text and given key with AES.
111-
func aesDecrypt(key, text []byte) ([]byte, error) {
112-
block, err := aes.NewCipher(key)
113-
if err != nil {
114-
return nil, err
115-
}
116-
if len(text) < aes.BlockSize {
117-
return nil, errors.New("ciphertext too short")
118-
}
119-
iv := text[:aes.BlockSize]
120-
text = text[aes.BlockSize:]
121-
cfb := cipher.NewCFBDecrypter(block, iv)
122-
cfb.XORKeyStream(text, text)
123-
data, err := base64.StdEncoding.DecodeString(string(text))
124-
if err != nil {
125-
return nil, err
126-
}
127-
return data, nil
128-
}
129-
13089
// NewTwoFactor creates a new two-factor authentication token.
13190
func NewTwoFactor(t *TwoFactor) error {
13291
_, err := x.Insert(t)

modules/secret/secret.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
package secret
66

77
import (
8+
"crypto/aes"
9+
"crypto/cipher"
810
"crypto/rand"
11+
"crypto/sha256"
912
"encoding/base64"
13+
"encoding/hex"
14+
"errors"
15+
"io"
1016
)
1117

1218
// New creats a new secret
@@ -31,3 +37,65 @@ func randomString(len int64) (string, error) {
3137
b, err := randomBytes(len)
3238
return base64.URLEncoding.EncodeToString(b), err
3339
}
40+
41+
// AesEncrypt encrypts text and given key with AES.
42+
func AesEncrypt(key, text []byte) ([]byte, error) {
43+
block, err := aes.NewCipher(key)
44+
if err != nil {
45+
return nil, err
46+
}
47+
b := base64.StdEncoding.EncodeToString(text)
48+
ciphertext := make([]byte, aes.BlockSize+len(b))
49+
iv := ciphertext[:aes.BlockSize]
50+
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
51+
return nil, err
52+
}
53+
cfb := cipher.NewCFBEncrypter(block, iv)
54+
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
55+
return ciphertext, nil
56+
}
57+
58+
// AesDecrypt decrypts text and given key with AES.
59+
func AesDecrypt(key, text []byte) ([]byte, error) {
60+
block, err := aes.NewCipher(key)
61+
if err != nil {
62+
return nil, err
63+
}
64+
if len(text) < aes.BlockSize {
65+
return nil, errors.New("ciphertext too short")
66+
}
67+
iv := text[:aes.BlockSize]
68+
text = text[aes.BlockSize:]
69+
cfb := cipher.NewCFBDecrypter(block, iv)
70+
cfb.XORKeyStream(text, text)
71+
data, err := base64.StdEncoding.DecodeString(string(text))
72+
if err != nil {
73+
return nil, err
74+
}
75+
return data, nil
76+
}
77+
78+
// EncryptSecret encrypts a string with given key into a hex string
79+
func EncryptSecret(key string, str string) (string, error) {
80+
keyHash := sha256.Sum256([]byte(key))
81+
plaintext := []byte(str)
82+
ciphertext, err := AesEncrypt(keyHash[:], plaintext)
83+
if err != nil {
84+
return "", err
85+
}
86+
return hex.EncodeToString(ciphertext), nil
87+
}
88+
89+
// DecryptSecret decrypts a previously encrypted hex string
90+
func DecryptSecret(key string, cipherhex string) (string, error) {
91+
keyHash := sha256.Sum256([]byte(key))
92+
ciphertext, err := hex.DecodeString(cipherhex)
93+
if err != nil {
94+
return "", err
95+
}
96+
plaintext, err := AesDecrypt(keyHash[:], ciphertext)
97+
if err != nil {
98+
return "", err
99+
}
100+
return string(plaintext), nil
101+
}

modules/secret/secret_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,16 @@ func TestNew(t *testing.T) {
2020
// check if secrets
2121
assert.NotEqual(t, result, result2)
2222
}
23+
24+
func TestEncryptDecrypt(t *testing.T) {
25+
var hex string
26+
var str string
27+
28+
hex, _ = EncryptSecret("foo", "baz")
29+
str, _ = DecryptSecret("foo", hex)
30+
assert.Equal(t, str, "baz")
31+
32+
hex, _ = EncryptSecret("bar", "baz")
33+
str, _ = DecryptSecret("foo", hex)
34+
assert.NotEqual(t, str, "baz")
35+
}

modules/setting/attachment.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package setting
66

77
import (
88
"path/filepath"
9-
"strings"
109

1110
"code.gitea.io/gitea/modules/log"
1211
)
@@ -65,7 +64,7 @@ func newAttachmentService() {
6564
Attachment.Minio.BasePath = sec.Key("MINIO_BASE_PATH").MustString("attachments/")
6665
}
6766

68-
Attachment.AllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png,application/zip,application/gzip"), "|", ",", -1)
67+
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".docx,.gif,.gz,.jpeg,.jpg,.log,.pdf,.png,.pptx,.txt,.xlsx,.zip")
6968
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(4)
7069
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
7170
Attachment.Enabled = sec.Key("ENABLED").MustBool(true)

modules/setting/repository.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ var (
5858
Upload struct {
5959
Enabled bool
6060
TempPath string
61-
AllowedTypes []string `delim:"|"`
61+
AllowedTypes string
6262
FileMaxSize int64
6363
MaxFiles int
6464
} `ini:"-"`
@@ -85,6 +85,10 @@ var (
8585
LockReasons []string
8686
} `ini:"repository.issue"`
8787

88+
Release struct {
89+
AllowedTypes string
90+
} `ini:"repository.release"`
91+
8892
Signing struct {
8993
SigningKey string
9094
SigningName string
@@ -165,13 +169,13 @@ var (
165169
Upload: struct {
166170
Enabled bool
167171
TempPath string
168-
AllowedTypes []string `delim:"|"`
172+
AllowedTypes string
169173
FileMaxSize int64
170174
MaxFiles int
171175
}{
172176
Enabled: true,
173177
TempPath: "data/tmp/uploads",
174-
AllowedTypes: []string{},
178+
AllowedTypes: "",
175179
FileMaxSize: 3,
176180
MaxFiles: 5,
177181
},
@@ -213,6 +217,12 @@ var (
213217
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
214218
},
215219

220+
Release: struct {
221+
AllowedTypes string
222+
}{
223+
AllowedTypes: "",
224+
},
225+
216226
// Signing settings
217227
Signing: struct {
218228
SigningKey string

modules/upload/filetype.go

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)