From cda44750cbdc7a8460666a4f0ac7f652d84a3964 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Mon, 5 Oct 2020 07:49:33 +0200
Subject: 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>
---
 modules/upload/upload.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 94 insertions(+)
 create mode 100644 modules/upload/upload.go

(limited to 'modules/upload/upload.go')

diff --git a/modules/upload/upload.go b/modules/upload/upload.go
new file mode 100644
index 0000000000..e020faca7e
--- /dev/null
+++ b/modules/upload/upload.go
@@ -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
+	}
+}
-- 
cgit v1.2.3