Skip to content

Commit

Permalink
Attachments: Add extension support, allow all types for releases (#12465
Browse files Browse the repository at this point in the history
)

* 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 <lauris@nix.lv>
  • Loading branch information
silverwind and lafriks authored Oct 5, 2020
1 parent 67a5573 commit cda4475
Show file tree
Hide file tree
Showing 26 changed files with 497 additions and 226 deletions.
13 changes: 8 additions & 5 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ LOCAL_COPY_PATH = tmp/local-repo
ENABLED = true
; Path for uploads. Defaults to `data/tmp/uploads` (tmp gets deleted on gitea restart)
TEMP_PATH = data/tmp/uploads
; One or more allowed types, e.g. image/jpeg|image/png. Nothing means any file type
; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
ALLOWED_TYPES =
; Max size of each file in megabytes. Defaults to 3MB
FILE_MAX_SIZE = 3
Expand Down Expand Up @@ -117,6 +117,10 @@ DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY=true
; List of reasons why a Pull Request or Issue can be locked
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam

[repository.release]
; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
ALLOWED_TYPES =

[repository.signing]
; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
; run in the context of the RUN_USER
Expand Down Expand Up @@ -766,11 +770,10 @@ DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false

[attachment]
; Whether attachments are enabled. Defaults to `true`
; Whether issue and pull request attachments are enabled. Defaults to `true`
ENABLED = true

; One or more allowed types, e.g. "image/jpeg|image/png". Use "*/*" for all types.
ALLOWED_TYPES = image/jpeg|image/png|application/zip|application/gzip
; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
ALLOWED_TYPES = .docx,.gif,.gz,.jpeg,.jpg,.log,.pdf,.png,.pptx,.txt,.xlsx,.zip
; Max size of each file. Defaults to 4MB
MAX_SIZE = 4
; Max number of files per upload. Defaults to 5
Expand Down
19 changes: 15 additions & 4 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.

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

### Repository - Upload (`repository.upload`)

- `ENABLED`: **true**: Whether repository file uploads are enabled
- `TEMP_PATH`: **data/tmp/uploads**: Path for uploads (tmp gets deleted on gitea restart)
- `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.
- `FILE_MAX_SIZE`: **3**: Max size of each file in megabytes.
- `MAX_FILES`: **5**: Max number of files per upload

### Repository - Release (`repository.release`)

- `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.

### Repository - Signing (`repository.signing`)

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

## Attachment (`attachment`)
## Issue and pull request attachments (`attachment`)

- `ENABLED`: **true**: Enable this to allow uploading attachments.
- `ALLOWED_TYPES`: **see app.example.ini**: Allowed MIME types, e.g. `image/jpeg|image/png`.
Use `*/*` for all types.
- `ENABLED`: **true**: Whether issue and pull request attachments are enabled.
- `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.
- `MAX_SIZE`: **4**: Maximum size (MB).
- `MAX_FILES`: **5**: Maximum number of attachments that can be uploaded at once.
- `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]`
Expand Down
2 changes: 1 addition & 1 deletion integrations/attachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func createAttachment(t *testing.T, session *TestSession, repoURL, filename stri

csrf := GetCSRF(t, session, repoURL)

req := NewRequestWithBody(t, "POST", "/attachments", body)
req := NewRequestWithBody(t, "POST", repoURL+"/issues/attachments", body)
req.Header.Add("X-Csrf-Token", csrf)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, expectedStatus)
Expand Down
51 changes: 5 additions & 46 deletions models/twofactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@
package models

import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"io"

"code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"

Expand Down Expand Up @@ -67,8 +63,8 @@ func (t *TwoFactor) getEncryptionKey() []byte {
}

// SetSecret sets the 2FA secret.
func (t *TwoFactor) SetSecret(secret string) error {
secretBytes, err := aesEncrypt(t.getEncryptionKey(), []byte(secret))
func (t *TwoFactor) SetSecret(secretString string) error {
secretBytes, err := secret.AesEncrypt(t.getEncryptionKey(), []byte(secretString))
if err != nil {
return err
}
Expand All @@ -82,51 +78,14 @@ func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
if err != nil {
return false, err
}
secret, err := aesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
if err != nil {
return false, err
}
secretStr := string(secret)
secretStr := string(secretBytes)
return totp.Validate(passcode, secretStr), nil
}

// aesEncrypt encrypts text and given key with AES.
func aesEncrypt(key, text []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
b := base64.StdEncoding.EncodeToString(text)
ciphertext := make([]byte, aes.BlockSize+len(b))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
cfb := cipher.NewCFBEncrypter(block, iv)
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
return ciphertext, nil
}

// aesDecrypt decrypts text and given key with AES.
func aesDecrypt(key, text []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(text) < aes.BlockSize {
return nil, errors.New("ciphertext too short")
}
iv := text[:aes.BlockSize]
text = text[aes.BlockSize:]
cfb := cipher.NewCFBDecrypter(block, iv)
cfb.XORKeyStream(text, text)
data, err := base64.StdEncoding.DecodeString(string(text))
if err != nil {
return nil, err
}
return data, nil
}

// NewTwoFactor creates a new two-factor authentication token.
func NewTwoFactor(t *TwoFactor) error {
_, err := x.Insert(t)
Expand Down
68 changes: 68 additions & 0 deletions modules/secret/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
package secret

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"io"
)

// New creats a new secret
Expand All @@ -31,3 +37,65 @@ func randomString(len int64) (string, error) {
b, err := randomBytes(len)
return base64.URLEncoding.EncodeToString(b), err
}

// AesEncrypt encrypts text and given key with AES.
func AesEncrypt(key, text []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
b := base64.StdEncoding.EncodeToString(text)
ciphertext := make([]byte, aes.BlockSize+len(b))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
cfb := cipher.NewCFBEncrypter(block, iv)
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
return ciphertext, nil
}

// AesDecrypt decrypts text and given key with AES.
func AesDecrypt(key, text []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(text) < aes.BlockSize {
return nil, errors.New("ciphertext too short")
}
iv := text[:aes.BlockSize]
text = text[aes.BlockSize:]
cfb := cipher.NewCFBDecrypter(block, iv)
cfb.XORKeyStream(text, text)
data, err := base64.StdEncoding.DecodeString(string(text))
if err != nil {
return nil, err
}
return data, nil
}

// EncryptSecret encrypts a string with given key into a hex string
func EncryptSecret(key string, str string) (string, error) {
keyHash := sha256.Sum256([]byte(key))
plaintext := []byte(str)
ciphertext, err := AesEncrypt(keyHash[:], plaintext)
if err != nil {
return "", err
}
return hex.EncodeToString(ciphertext), nil
}

// DecryptSecret decrypts a previously encrypted hex string
func DecryptSecret(key string, cipherhex string) (string, error) {
keyHash := sha256.Sum256([]byte(key))
ciphertext, err := hex.DecodeString(cipherhex)
if err != nil {
return "", err
}
plaintext, err := AesDecrypt(keyHash[:], ciphertext)
if err != nil {
return "", err
}
return string(plaintext), nil
}
13 changes: 13 additions & 0 deletions modules/secret/secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ func TestNew(t *testing.T) {
// check if secrets
assert.NotEqual(t, result, result2)
}

func TestEncryptDecrypt(t *testing.T) {
var hex string
var str string

hex, _ = EncryptSecret("foo", "baz")
str, _ = DecryptSecret("foo", hex)
assert.Equal(t, str, "baz")

hex, _ = EncryptSecret("bar", "baz")
str, _ = DecryptSecret("foo", hex)
assert.NotEqual(t, str, "baz")
}
3 changes: 1 addition & 2 deletions modules/setting/attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package setting

import (
"path/filepath"
"strings"

"code.gitea.io/gitea/modules/log"
)
Expand Down Expand Up @@ -65,7 +64,7 @@ func newAttachmentService() {
Attachment.Minio.BasePath = sec.Key("MINIO_BASE_PATH").MustString("attachments/")
}

Attachment.AllowedTypes = strings.Replace(sec.Key("ALLOWED_TYPES").MustString("image/jpeg,image/png,application/zip,application/gzip"), "|", ",", -1)
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".docx,.gif,.gz,.jpeg,.jpg,.log,.pdf,.png,.pptx,.txt,.xlsx,.zip")
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(4)
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
Expand Down
16 changes: 13 additions & 3 deletions modules/setting/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ var (
Upload struct {
Enabled bool
TempPath string
AllowedTypes []string `delim:"|"`
AllowedTypes string
FileMaxSize int64
MaxFiles int
} `ini:"-"`
Expand All @@ -85,6 +85,10 @@ var (
LockReasons []string
} `ini:"repository.issue"`

Release struct {
AllowedTypes string
} `ini:"repository.release"`

Signing struct {
SigningKey string
SigningName string
Expand Down Expand Up @@ -165,13 +169,13 @@ var (
Upload: struct {
Enabled bool
TempPath string
AllowedTypes []string `delim:"|"`
AllowedTypes string
FileMaxSize int64
MaxFiles int
}{
Enabled: true,
TempPath: "data/tmp/uploads",
AllowedTypes: []string{},
AllowedTypes: "",
FileMaxSize: 3,
MaxFiles: 5,
},
Expand Down Expand Up @@ -213,6 +217,12 @@ var (
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
},

Release: struct {
AllowedTypes string
}{
AllowedTypes: "",
},

// Signing settings
Signing: struct {
SigningKey string
Expand Down
46 changes: 0 additions & 46 deletions modules/upload/filetype.go

This file was deleted.

Loading

0 comments on commit cda4475

Please sign in to comment.