From 8de83a1de104657771e87d56d1f7d97d275fe13f Mon Sep 17 00:00:00 2001 From: Scott Bishel Date: Wed, 29 Mar 2023 08:37:13 -0600 Subject: [PATCH] copy safe writeFileResponse into personal server/desktop (#4654) (#4664) * copy safe writeFileResponse into personal server/desktop * lint fix --------- Co-authored-by: Mattermost Build (cherry picked from commit d8e1fb4832953fb31c54d4bc3542e2e4d4fe0fb7) Co-authored-by: Mattermost Build --- server/api/files.go | 91 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/server/api/files.go b/server/api/files.go index d9f6aa109..9adb54f0a 100644 --- a/server/api/files.go +++ b/server/api/files.go @@ -8,6 +8,8 @@ import ( "errors" "io" "net/http" + "net/url" + "strconv" "strings" "time" @@ -20,9 +22,30 @@ import ( mmModel "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/mlog" - "github.com/mattermost/mattermost-server/v6/shared/web" ) +var UnsafeContentTypes = [...]string{ + "application/javascript", + "application/ecmascript", + "text/javascript", + "text/ecmascript", + "application/x-javascript", + "text/html", +} + +var MediaContentTypes = [...]string{ + "image/jpeg", + "image/png", + "image/bmp", + "image/gif", + "image/tiff", + "video/avi", + "video/mpeg", + "video/mp4", + "audio/mpeg", + "audio/wav", +} + // FileUploadResponse is the response to a file upload // swagger:model type FileUploadResponse struct { @@ -170,10 +193,74 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { } defer fileReader.Close() - web.WriteFileResponse(filename, fileInfo.MimeType, fileInfo.Size, time.Now(), "", fileReader, false, w, r) + mimeType := "" + var fileSize int64 + if fileInfo != nil { + mimeType = fileInfo.MimeType + fileSize = fileInfo.Size + } + writeFileResponse(filename, mimeType, fileSize, time.Now(), "", fileReader, false, w, r) auditRec.Success() } +func writeFileResponse(filename string, contentType string, contentSize int64, + lastModification time.Time, webserverMode string, fileReader io.ReadSeeker, forceDownload bool, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "private, no-cache") + w.Header().Set("X-Content-Type-Options", "nosniff") + + if contentSize > 0 { + contentSizeStr := strconv.Itoa(int(contentSize)) + if webserverMode == "gzip" { + w.Header().Set("X-Uncompressed-Content-Length", contentSizeStr) + } else { + w.Header().Set("Content-Length", contentSizeStr) + } + } + + if contentType == "" { + contentType = "application/octet-stream" + } else { + for _, unsafeContentType := range UnsafeContentTypes { + if strings.HasPrefix(contentType, unsafeContentType) { + contentType = "text/plain" + break + } + } + } + + w.Header().Set("Content-Type", contentType) + + var toDownload bool + if forceDownload { + toDownload = true + } else { + isMediaType := false + + for _, mediaContentType := range MediaContentTypes { + if strings.HasPrefix(contentType, mediaContentType) { + isMediaType = true + break + } + } + + toDownload = !isMediaType + } + + filename = url.PathEscape(filename) + + if toDownload { + w.Header().Set("Content-Disposition", "attachment;filename=\""+filename+"\"; filename*=UTF-8''"+filename) + } else { + w.Header().Set("Content-Disposition", "inline;filename=\""+filename+"\"; filename*=UTF-8''"+filename) + } + + // prevent file links from being embedded in iframes + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Content-Security-Policy", "Frame-ancestors 'none'") + + http.ServeContent(w, r, filename, lastModification, fileReader) +} + func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) { // swagger:operation GET /files/teams/{teamID}/{boardID}/{filename}/info getFile //