mattermost/server/channels/api4/upload.go
Jesse Hallam c462720186
[MM-65822] Add directory conflict checks for plugin and import uploads (#34681)
* [MM-66789] Add directory conflict checks for plugin and import uploads

Prevent security issues where plugin uploads could write into the import
directory (or subdirectories) and vice versa by adding validation checks
at the REST API level when uploading plugins or creating import uploads.

* improved handling of root directories

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-03-03 16:31:05 -04:00

207 lines
6.2 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"errors"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
)
func (api *API) InitUpload() {
api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload, handlerParamFileAPI)).Methods(http.MethodPost)
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(getUpload)).Methods(http.MethodGet)
api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData, handlerParamFileAPI)).Methods(http.MethodPost)
}
func createUpload(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("createUpload",
"api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
var us model.UploadSession
if jsonErr := json.NewDecoder(r.Body).Decode(&us); jsonErr != nil {
c.SetInvalidParamWithErr("upload", jsonErr)
return
}
// these are not supported for client uploads; shared channels only.
us.RemoteId = ""
us.ReqFileId = ""
us.Filename = filepath.Base(us.Filename)
auditRec := c.MakeAuditRecord(model.AuditEventCreateUpload, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterAuditableToAuditRec(auditRec, "upload", &us)
if us.Type == model.UploadTypeImport {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Srv().License().IsCloud() {
c.Err = model.NewAppError("createUpload", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
return
}
conflict, err := fileutils.CheckDirectoryConflict(*c.App.Config().ImportSettings.Directory, *c.App.Config().PluginSettings.Directory)
if err != nil {
c.Err = model.NewAppError("createUpload", "api.upload.create.check_directory.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
if conflict {
c.Err = model.NewAppError("createUpload", "api.upload.create.directory_conflict.app_error", nil, "", http.StatusForbidden)
return
}
} else {
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile); !ok {
c.SetPermissionError(model.PermissionUploadFile)
return
}
us.Type = model.UploadTypeAttachment
}
us.Id = model.NewId()
if c.AppContext.Session().UserId != "" {
us.UserId = c.AppContext.Session().UserId
}
if us.FileSize > *c.App.Config().FileSettings.MaxFileSize {
c.Err = model.NewAppError("createUpload", "api.upload.create.upload_too_large.app_error",
map[string]any{"channelId": us.ChannelId}, "", http.StatusRequestEntityTooLarge)
return
}
rus, err := c.App.CreateUploadSession(c.AppContext, &us)
if err != nil {
c.Err = err
return
}
auditRec.Success()
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(rus); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func getUpload(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUploadId()
if c.Err != nil {
return
}
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.UserId != c.AppContext.Session().UserId && !c.IsSystemAdmin() {
c.Err = model.NewAppError("getUpload", "api.upload.get_upload.forbidden.app_error", nil, "", http.StatusForbidden)
return
}
if err := json.NewEncoder(w).Encode(us); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func uploadData(c *Context, w http.ResponseWriter, r *http.Request) {
if !*c.App.Config().FileSettings.EnableFileAttachments {
c.Err = model.NewAppError("uploadData", "api.file.attachments.disabled.app_error",
nil, "", http.StatusNotImplemented)
return
}
c.RequireUploadId()
if c.Err != nil {
return
}
auditRec := c.MakeAuditRecord(model.AuditEventUploadData, model.AuditStatusFail)
defer c.LogAuditRec(auditRec)
model.AddEventParameterToAuditRec(auditRec, "upload_id", c.Params.UploadId)
c.AppContext = c.AppContext.With(app.RequestContextWithMaster)
us, err := c.App.GetUploadSession(c.AppContext, c.Params.UploadId)
if err != nil {
c.Err = err
return
}
if us.Type == model.UploadTypeImport {
if !c.IsSystemAdmin() {
c.SetPermissionError(model.PermissionManageSystem)
return
}
if c.App.Srv().License().IsCloud() {
c.Err = model.NewAppError("UploadData", "api.file.cloud_upload.app_error", nil, "", http.StatusBadRequest)
return
}
} else {
if us.UserId != c.AppContext.Session().UserId {
c.SetPermissionError(model.PermissionUploadFile)
return
} else if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), us.ChannelId, model.PermissionUploadFile); !ok {
c.SetPermissionError(model.PermissionUploadFile)
return
}
}
info, err := doUploadData(c, us, r)
if err != nil {
c.Err = err
return
}
auditRec.Success()
if info == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if err := json.NewEncoder(w).Encode(info); err != nil {
c.Logger.Warn("Error while writing response", mlog.Err(err))
}
}
func doUploadData(c *Context, us *model.UploadSession, r *http.Request) (*model.FileInfo, *model.AppError) {
boundary, parseErr := parseMultipartRequestHeader(r)
if parseErr != nil && !errors.Is(parseErr, http.ErrNotMultipart) {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_type",
nil, parseErr.Error(), http.StatusBadRequest)
}
var rd io.Reader
if boundary != "" {
mr := multipart.NewReader(r.Body, boundary)
p, partErr := mr.NextPart()
if partErr != nil {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.multipart_error",
nil, partErr.Error(), http.StatusBadRequest)
}
rd = p
} else {
if r.ContentLength > (us.FileSize - us.FileOffset) {
return nil, model.NewAppError("uploadData", "api.upload.upload_data.invalid_content_length",
nil, "", http.StatusBadRequest)
}
rd = r.Body
}
return c.App.UploadData(c.AppContext, us, rd)
}