mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-13 04:57:45 -04:00
* [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>
207 lines
6.2 KiB
Go
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)
|
|
}
|