VYPR
Low severityNVD Advisory· Published Apr 26, 2024· Updated Aug 1, 2024

Excessive resource consumption due to lack to request path size limits

CVE-2024-22091

Description

Mattermost versions 8.1.x <= 8.1.10, 9.6.x <= 9.6.0, 9.5.x <= 9.5.2 and 8.1.x <= 8.1.11 fail to limit the size of a request path that includes user inputs which allows an attacker to cause excessive resource consumption, possibly leading to a DoS via sending large request paths

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost-serverGo
>= 8.1.0, < 8.1.128.1.12
github.com/mattermost/mattermost-serverGo
>= 9.5.0, < 9.5.39.5.3
github.com/mattermost/mattermost-serverGo
>= 9.6.0-rc1, < 9.6.19.6.1

Affected products

1

Patches

4
49e7c477246e

Cherrypicking API Handler params for 8.1 (#26564)

https://github.com/mattermost/mattermostHarshil SharmaMar 22, 2024via ghsa
30 files changed · +307 181
  • server/channels/api4/brand.go+1 1 modified
    @@ -13,7 +13,7 @@ import (
     
     func (api *API) InitBrand() {
     	api.BaseRoutes.Brand.Handle("/image", api.APIHandlerTrustRequester(getBrandImage)).Methods("GET")
    -	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage)).Methods("POST")
    +	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(deleteBrandImage)).Methods("DELETE")
     }
     
    
  • server/channels/api4/channel_category.go+7 4 modified
    @@ -118,7 +118,11 @@ func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *htt
     	auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail)
     	defer c.LogAuditRec(auditRec)
     
    -	categoryOrder := model.ArrayFromJSON(r.Body)
    +	categoryOrder, err := model.NonSortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("updateCategoryOrderForTeamForUser", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	}
     
     	for _, categoryId := range categoryOrder {
     		if !c.App.SessionHasPermissionToCategory(c.AppContext, *c.AppContext.Session(), c.Params.UserId, c.Params.TeamId, categoryId) {
    @@ -127,9 +131,8 @@ func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *htt
     		}
     	}
     
    -	err := c.App.UpdateSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId, categoryOrder)
    -	if err != nil {
    -		c.Err = err
    +	if appErr := c.App.UpdateSidebarCategoryOrder(c.AppContext, c.Params.UserId, c.Params.TeamId, categoryOrder); appErr != nil {
    +		c.Err = appErr
     		return
     	}
     
    
  • server/channels/api4/channel.go+26 8 modified
    @@ -414,9 +414,19 @@ func restoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds := model.ArrayFromJSON(r.Body)
    +	userIds, jsonErr := model.NonSortedArrayFromJSON(r.Body)
    +	if jsonErr != nil {
    +		c.Err = model.NewAppError("createDirectChannel", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(jsonErr)
    +		return
    +	}
     	allowed := false
     
    +	// single userId allowed if creating a self-channel
    +	// NonSortedArrayFromJSON will remove duplicates, so need to add back
    +	if len(userIds) == 1 && userIds[0] == c.AppContext.Session().UserId {
    +		userIds = append(userIds, userIds[0])
    +	}
    +
     	if len(userIds) != 2 {
     		c.SetInvalidParam("user_ids")
     		return
    @@ -500,9 +510,11 @@ func searchGroupChannels(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func createGroupChannel(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds := model.ArrayFromJSON(r.Body)
    -
    -	if len(userIds) == 0 {
    +	userIds, jsonErr := model.SortedArrayFromJSON(r.Body)
    +	if jsonErr != nil {
    +		c.Err = model.NewAppError("createGroupChannel", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(jsonErr)
    +		return
    +	} else if len(userIds) == 0 {
     		c.SetInvalidParam("user_ids")
     		return
     	}
    @@ -857,8 +869,11 @@ func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Re
     		return
     	}
     
    -	channelIds := model.ArrayFromJSON(r.Body)
    -	if len(channelIds) == 0 {
    +	channelIds, jsonErr := model.SortedArrayFromJSON(r.Body)
    +	if jsonErr != nil {
    +		c.Err = model.NewAppError("getPublicChannelsByIdsForTeam", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(jsonErr)
    +		return
    +	} else if len(channelIds) == 0 {
     		c.SetInvalidParam("channel_ids")
     		return
     	}
    @@ -1397,8 +1412,11 @@ func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request)
     		return
     	}
     
    -	userIds := model.ArrayFromJSON(r.Body)
    -	if len(userIds) == 0 {
    +	userIds, jsonErr := model.SortedArrayFromJSON(r.Body)
    +	if jsonErr != nil {
    +		c.Err = model.NewAppError("getChannelMembersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(jsonErr)
    +		return
    +	} else if len(userIds) == 0 {
     		c.SetInvalidParam("user_ids")
     		return
     	}
    
  • server/channels/api4/data_retention.go+24 28 modified
    @@ -264,10 +264,9 @@ func searchTeamsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	var teamIDs []string
    -	jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
    -	if jsonErr != nil {
    -		c.SetInvalidParamWithErr("team_ids", jsonErr)
    +	teamIDs, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("addTeamsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
     	}
     	auditRec := c.MakeAuditRecord("addTeamsToPolicy", audit.Fail)
    @@ -279,9 +278,9 @@ func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	err := c.App.AddTeamsToRetentionPolicy(policyId, teamIDs)
    -	if err != nil {
    -		c.Err = err
    +	appErr := c.App.AddTeamsToRetentionPolicy(policyId, teamIDs)
    +	if appErr != nil {
    +		c.Err = appErr
     		return
     	}
     
    @@ -292,10 +291,9 @@ func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	var teamIDs []string
    -	jsonErr := json.NewDecoder(r.Body).Decode(&teamIDs)
    -	if jsonErr != nil {
    -		c.SetInvalidParamWithErr("team_ids", jsonErr)
    +	teamIDs, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("removeTeamsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
     	}
     	auditRec := c.MakeAuditRecord("removeTeamsFromPolicy", audit.Fail)
    @@ -308,9 +306,9 @@ func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	err := c.App.RemoveTeamsFromRetentionPolicy(policyId, teamIDs)
    -	if err != nil {
    -		c.Err = err
    +	appErr := c.App.RemoveTeamsFromRetentionPolicy(policyId, teamIDs)
    +	if appErr != nil {
    +		c.Err = appErr
     		return
     	}
     
    @@ -385,10 +383,9 @@ func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request)
     func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	var channelIDs []string
    -	jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
    -	if jsonErr != nil {
    -		c.SetInvalidParamWithErr("channel_ids", jsonErr)
    +	channelIDs, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("addChannelsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
     	}
     	auditRec := c.MakeAuditRecord("addChannelsToPolicy", audit.Fail)
    @@ -401,9 +398,9 @@ func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	err := c.App.AddChannelsToRetentionPolicy(policyId, channelIDs)
    -	if err != nil {
    -		c.Err = err
    +	appErr := c.App.AddChannelsToRetentionPolicy(policyId, channelIDs)
    +	if appErr != nil {
    +		c.Err = appErr
     		return
     	}
     
    @@ -414,10 +411,9 @@ func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	var channelIDs []string
    -	jsonErr := json.NewDecoder(r.Body).Decode(&channelIDs)
    -	if jsonErr != nil {
    -		c.SetInvalidParamWithErr("channel_ids", jsonErr)
    +	channelIDs, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("removeChannelsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
     	}
     	auditRec := c.MakeAuditRecord("removeChannelsFromPolicy", audit.Fail)
    @@ -430,9 +426,9 @@ func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request
     		return
     	}
     
    -	err := c.App.RemoveChannelsFromRetentionPolicy(policyId, channelIDs)
    -	if err != nil {
    -		c.Err = err
    +	appErr := c.App.RemoveChannelsFromRetentionPolicy(policyId, channelIDs)
    +	if appErr != nil {
    +		c.Err = appErr
     		return
     	}
     
    
  • server/channels/api4/emoji.go+1 1 modified
    @@ -20,7 +20,7 @@ const (
     )
     
     func (api *API) InitEmoji() {
    -	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji)).Methods("POST")
    +	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods("GET")
     	api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods("POST")
     	api.BaseRoutes.Emojis.Handle("/autocomplete", api.APISessionRequired(autocompleteEmojis)).Methods("GET")
    
  • server/channels/api4/file.go+1 1 modified
    @@ -32,7 +32,7 @@ const (
     const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
     
     func (api *API) InitFile() {
    -	api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST")
    +	api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Files.Handle("/search", api.APISessionRequired(searchFilesForUser)).Methods("POST")
     	api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET")
     	api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
    
  • server/channels/api4/handlers.go+45 9 modified
    @@ -16,9 +16,15 @@ type Context = web.Context
     
     type handlerFunc func(*Context, http.ResponseWriter, *http.Request)
     
    +type APIHandlerOption string
    +
    +const (
    +	handlerParamFileAPI = APIHandlerOption("fileAPI")
    +)
    +
     // APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
     // granted.
    -func (api *API) APIHandler(h handlerFunc) http.Handler {
    +func (api *API) APIHandler(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -29,6 +35,8 @@ func (api *API) APIHandler(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -37,7 +45,7 @@ func (api *API) APIHandler(h handlerFunc) http.Handler {
     
     // APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
     // be granted.
    -func (api *API) APISessionRequired(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -48,6 +56,8 @@ func (api *API) APISessionRequired(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -56,7 +66,7 @@ func (api *API) APISessionRequired(h handlerFunc) http.Handler {
     }
     
     // CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
    -func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
    +func (api *API) CloudAPIKeyRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:             api.srv,
     		HandleFunc:      h,
    @@ -68,6 +78,8 @@ func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
     		IsStatic:        false,
     		IsLocal:         false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -76,7 +88,7 @@ func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
     }
     
     // RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints.
    -func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
    +func (api *API) RemoteClusterTokenRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:                       api.srv,
     		HandleFunc:                h,
    @@ -89,6 +101,8 @@ func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
     		IsStatic:                  false,
     		IsLocal:                   false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -98,7 +112,7 @@ func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
     // APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session  but when accessed,
     // if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
     // authentication must be waived.
    -func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredMfa(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -109,6 +123,8 @@ func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -119,7 +135,7 @@ func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
     // APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
     // allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
     // websocket.
    -func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
    +func (api *API) APIHandlerTrustRequester(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -130,6 +146,8 @@ func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -139,7 +157,7 @@ func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
     
     // APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and
     // are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads.
    -func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredTrustRequester(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -150,6 +168,8 @@ func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -159,7 +179,7 @@ func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
     
     // DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load,
     // responding with HTTP 503 (Service Unavailable).
    -func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:             api.srv,
     		HandleFunc:      h,
    @@ -171,6 +191,8 @@ func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
     		IsLocal:         false,
     		DisableWhenBusy: true,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
     	}
    @@ -182,7 +204,7 @@ func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
     // mode, this is, through a UNIX socket and without an authenticated
     // session, but with one that has no user set and no permission
     // restrictions
    -func (api *API) APILocal(h handlerFunc) http.Handler {
    +func (api *API) APILocal(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -193,6 +215,7 @@ func (api *API) APILocal(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        true,
     	}
    +	setHandlerOpts(handler, opts...)
     
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gziphandler.GzipHandler(handler)
    @@ -224,3 +247,16 @@ func rejectGuests(c *Context) *model.AppError {
     	}
     	return nil
     }
    +
    +func setHandlerOpts(handler *web.Handler, opts ...APIHandlerOption) {
    +	if len(opts) == 0 {
    +		return
    +	}
    +
    +	for _, option := range opts {
    +		switch option {
    +		case handlerParamFileAPI:
    +			handler.FileAPI = true
    +		}
    +	}
    +}
    
  • server/channels/api4/ldap.go+2 2 modified
    @@ -34,8 +34,8 @@ func (api *API) InitLdap() {
     	// DELETE /api/v4/ldap/groups/:remote_id/link
     	api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods("DELETE")
     
    -	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate)).Methods("POST")
    -	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate)).Methods("POST")
    +	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate, handlerParamFileAPI)).Methods("POST")
    +	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate, handlerParamFileAPI)).Methods("POST")
     
     	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
     	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
    
  • server/channels/api4/license.go+1 1 modified
    @@ -21,7 +21,7 @@ import (
     func (api *API) InitLicense() {
     	api.BaseRoutes.APIRoot.Handle("/trial-license", api.APISessionRequired(requestTrialLicense)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET")
    -	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense)).Methods("POST")
    +	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
     	api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
     	api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
    
  • server/channels/api4/license_local.go+1 1 modified
    @@ -15,7 +15,7 @@ import (
     )
     
     func (api *API) InitLicenseLocal() {
    -	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense)).Methods("POST")
    +	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localRemoveLicense)).Methods("DELETE")
     }
     
    
  • server/channels/api4/plugin.go+1 1 modified
    @@ -24,7 +24,7 @@ const (
     )
     
     func (api *API) InitPlugin() {
    -	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin)).Methods("POST")
    +	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(getPlugins)).Methods("GET")
     	api.BaseRoutes.Plugin.Handle("", api.APISessionRequired(removePlugin)).Methods("DELETE")
     	api.BaseRoutes.Plugins.Handle("/install_from_url", api.APISessionRequired(installPluginFromURL)).Methods("POST")
    
  • server/channels/api4/plugin_local.go+1 1 modified
    @@ -4,7 +4,7 @@
     package api4
     
     func (api *API) InitPluginLocal() {
    -	api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin)).Methods("POST")
    +	api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Plugins.Handle("", api.APILocal(getPlugins)).Methods("GET")
     	api.BaseRoutes.Plugins.Handle("/install_from_url", api.APILocal(installPluginFromURL)).Methods("POST")
     	api.BaseRoutes.Plugin.Handle("", api.APILocal(removePlugin)).Methods("DELETE")
    
  • server/channels/api4/post.go+5 3 modified
    @@ -502,9 +502,11 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
     
     // getPostsByIds also sets a header to indicate, if posts were truncated as per the cloud plan's limit.
     func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	postIDs := model.ArrayFromJSON(r.Body)
    -
    -	if len(postIDs) == 0 {
    +	postIDs, jsonErr := model.SortedArrayFromJSON(r.Body)
    +	if jsonErr != nil {
    +		c.Err = model.NewAppError("getPostsByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(jsonErr)
    +		return
    +	} else if len(postIDs) == 0 {
     		c.SetInvalidParam("post_ids")
     		return
     	}
    
  • server/channels/api4/preference.go+2 2 modified
    @@ -103,7 +103,7 @@ func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	var preferences model.Preferences
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, &preferences)
    +	err := model.StructFromJSONLimited(r.Body, &preferences)
     	if err != nil {
     		c.SetInvalidParamWithErr("preferences", err)
     		return
    @@ -155,7 +155,7 @@ func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	var preferences model.Preferences
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, &preferences)
    +	err := model.StructFromJSONLimited(r.Body, &preferences)
     	if err != nil {
     		c.SetInvalidParamWithErr("preferences", err)
     		return
    
  • server/channels/api4/reaction.go+5 1 modified
    @@ -119,7 +119,11 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
    -	postIds := model.ArrayFromJSON(r.Body)
    +	postIds, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("getBulkReactions", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	}
     	for _, postId := range postIds {
     		if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), postId, model.PermissionReadChannelContent) {
     			c.SetPermissionError(model.PermissionReadChannelContent)
    
  • server/channels/api4/remote_cluster.go+2 2 modified
    @@ -20,8 +20,8 @@ func (api *API) InitRemoteCluster() {
     	api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods("POST")
     	api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods("POST")
     	api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
    -	api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData)).Methods("POST")
    -	api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage)).Methods("POST")
    +	api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData, handlerParamFileAPI)).Methods("POST")
    +	api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage, handlerParamFileAPI)).Methods("POST")
     }
     
     func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
    
  • server/channels/api4/role.go+5 3 modified
    @@ -84,9 +84,11 @@ func getRoleByName(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getRolesByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	rolenames := model.ArrayFromJSON(r.Body)
    -
    -	if len(rolenames) == 0 {
    +	rolenames, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("getRolesByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	} else if len(rolenames) == 0 {
     		c.SetInvalidParam("rolenames")
     		return
     	}
    
  • server/channels/api4/role_test.go+2 1 modified
    @@ -5,6 +5,7 @@ package api4
     
     import (
     	"context"
    +	"fmt"
     	"sort"
     	"strings"
     	"testing"
    @@ -189,7 +190,7 @@ func TestGetRolesByNames(t *testing.T) {
     		// too many roles should error with bad request
     		roles := []string{}
     		for i := 0; i < GetRolesByNamesMax+10; i++ {
    -			roles = append(roles, role1.Name)
    +			roles = append(roles, fmt.Sprintf("role1.Name%v", i))
     		}
     
     		_, resp, err := client.GetRolesByNames(context.Background(), roles)
    
  • server/channels/api4/status.go+5 3 modified
    @@ -49,9 +49,11 @@ func getUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUserStatusesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds := model.ArrayFromJSON(r.Body)
    -
    -	if len(userIds) == 0 {
    +	userIds, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("getUserStatusesByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	} else if len(userIds) == 0 {
     		c.SetInvalidParam("user_ids")
     		return
     	}
    
  • server/channels/api4/system.go+5 1 modified
    @@ -870,7 +870,11 @@ func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Reque
     	defer c.LogAuditRec(auditRec)
     	c.LogAudit("attempt")
     
    -	ids := model.ArrayFromJSON(r.Body)
    +	ids, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("updateViewedProductNotices", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	}
     	appErr := c.App.UpdateViewedProductNotices(c.AppContext.Session().UserId, ids)
     	if appErr != nil {
     		c.Err = appErr
    
  • server/channels/api4/team.go+8 6 modified
    @@ -49,7 +49,7 @@ func (api *API) InitTeam() {
     	api.BaseRoutes.Team.Handle("/regenerate_invite_id", api.APISessionRequired(regenerateTeamInviteId)).Methods("POST")
     
     	api.BaseRoutes.Team.Handle("/image", api.APISessionRequiredTrustRequester(getTeamIcon)).Methods("GET")
    -	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon)).Methods("POST")
    +	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(removeTeamIcon)).Methods("DELETE")
     
     	api.BaseRoutes.TeamMembers.Handle("", api.APISessionRequired(getTeamMembers)).Methods("GET")
    @@ -644,10 +644,12 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	var userIDs []string
    -	err := json.NewDecoder(r.Body).Decode(&userIDs)
    -	if err != nil || len(userIDs) == 0 {
    -		c.SetInvalidParamWithErr("user_ids", err)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("getTeamMembersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	} else if len(userIDs) == 0 {
    +		c.SetInvalidParam("user_ids")
     		return
     	}
     
    @@ -1374,7 +1376,7 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	memberInvite := &model.MemberInvite{}
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, memberInvite)
    +	err := model.StructFromJSONLimited(r.Body, memberInvite)
     	if err != nil {
     		c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/team_local.go+1 1 modified
    @@ -79,7 +79,7 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
     	}
     
     	memberInvite := &model.MemberInvite{}
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, memberInvite)
    +	err := model.StructFromJSONLimited(r.Body, memberInvite)
     	if err != nil {
     		c.Err = model.NewAppError("Api4.localInviteUsersToTeam", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/upload.go+2 2 modified
    @@ -17,9 +17,9 @@ import (
     )
     
     func (api *API) InitUpload() {
    -	api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload)).Methods("POST")
    +	api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(getUpload)).Methods("GET")
    -	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData)).Methods("POST")
    +	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData, handlerParamFileAPI)).Methods("POST")
     }
     
     func createUpload(c *Context, w http.ResponseWriter, r *http.Request) {
    
  • server/channels/api4/upload_local.go+2 2 modified
    @@ -4,7 +4,7 @@
     package api4
     
     func (api *API) InitUploadLocal() {
    -	api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload)).Methods("POST")
    +	api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Upload.Handle("", api.APILocal(getUpload)).Methods("GET")
    -	api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData)).Methods("POST")
    +	api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData, handlerParamFileAPI)).Methods("POST")
     }
    
  • server/channels/api4/user.go+18 12 modified
    @@ -36,7 +36,7 @@ func (api *API) InitUser() {
     	api.BaseRoutes.User.Handle("", api.APISessionRequired(getUser)).Methods("GET")
     	api.BaseRoutes.User.Handle("/image/default", api.APISessionRequiredTrustRequester(getDefaultProfileImage)).Methods("GET")
     	api.BaseRoutes.User.Handle("/image", api.APISessionRequiredTrustRequester(getProfileImage)).Methods("GET")
    -	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage)).Methods("POST")
    +	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setDefaultProfileImage)).Methods("DELETE")
     	api.BaseRoutes.User.Handle("", api.APISessionRequired(updateUser)).Methods("PUT")
     	api.BaseRoutes.User.Handle("/patch", api.APISessionRequired(patchUser)).Methods("PUT")
    @@ -618,9 +618,11 @@ func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	channelIds := model.ArrayFromJSON(r.Body)
    -
    -	if len(channelIds) == 0 {
    +	channelIds, jsonErr := model.SortedArrayFromJSON(r.Body)
    +	if jsonErr != nil || len(channelIds) == 0 {
    +		c.Err = model.NewAppError("getUsersByGroupChannelIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(jsonErr)
    +		return
    +	} else if len(channelIds) == 0 {
     		c.SetInvalidParam("channel_ids")
     		return
     	}
    @@ -948,10 +950,12 @@ func requireGroupAccess(c *web.Context, groupID string) *model.AppError {
     }
     
     func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	var userIDs []string
    -	err := json.NewDecoder(r.Body).Decode(&userIDs)
    -	if err != nil || len(userIDs) == 0 {
    -		c.SetInvalidParamWithErr("user_ids", err)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("getUsersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	} else if len(userIDs) == 0 {
    +		c.SetInvalidParam("user_ids")
     		return
     	}
     
    @@ -997,10 +1001,12 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	var usernames []string
    -	err := json.NewDecoder(r.Body).Decode(&usernames)
    -	if err != nil || len(usernames) == 0 {
    -		c.SetInvalidParamWithErr("usernames", err)
    +	usernames, err := model.SortedArrayFromJSON(r.Body)
    +	if err != nil {
    +		c.Err = model.NewAppError("getUsersByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
    +		return
    +	} else if len(usernames) == 0 {
    +		c.SetInvalidParam("usernames")
     		return
     	}
     
    
  • server/channels/api4/user_local.go+5 3 modified
    @@ -231,9 +231,11 @@ func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func localGetUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIDs := model.ArrayFromJSON(r.Body)
    -
    -	if len(userIDs) == 0 {
    +	userIDs, jsonErr := model.SortedArrayFromJSON(r.Body)
    +	if jsonErr != nil {
    +		c.Err = model.NewAppError("localGetUsersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(jsonErr)
    +		return
    +	} else if len(userIDs) == 0 {
     		c.SetInvalidParam("user_ids")
     		return
     	}
    
  • server/channels/web/handlers.go+26 8 modified
    @@ -31,7 +31,8 @@ import (
     )
     
     const (
    -	frameAncestors = "'self' teams.microsoft.com"
    +	frameAncestors   = "'self' teams.microsoft.com"
    +	maxURLCharacters = 2048
     )
     
     func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
    @@ -86,6 +87,7 @@ type Handler struct {
     	IsStatic                  bool
     	IsLocal                   bool
     	DisableWhenBusy           bool
    +	FileAPI                   bool
     
     	cspShaDirective string
     }
    @@ -142,7 +144,20 @@ func generateDevCSP(c Context) string {
     	return " " + strings.Join(devCSP, " ")
     }
     
    +func (h Handler) basicSecurityChecks(w http.ResponseWriter, r *http.Request) *model.AppError {
    +	if len(r.RequestURI) > maxURLCharacters {
    +		return model.NewAppError("basicSecurityChecks", "basic_security_check.url.too_long_error", nil, "", http.StatusRequestURITooLong)
    +	}
    +
    +	return nil
    +}
    +
     func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    +	if appErr := h.basicSecurityChecks(w, r); appErr != nil {
    +		http.Error(w, appErr.Error(), appErr.StatusCode)
    +		return
    +	}
    +
     	w = newWrappedWriter(w)
     	now := time.Now()
     
    @@ -205,13 +220,16 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     		c.App = app_opentracing.NewOpenTracingAppLayer(c.App, ctx)
     	}
     
    -	// Set the max request body size to be equal to MaxFileSize.
    -	// Ideally, non-file request bodies should be smaller than file request bodies,
    -	// but we don't have a clean way to identify all file upload handlers.
    -	// So to keep it simple, we clamp it to the max file size.
    -	// We add a buffer of bytes.MinRead so that file sizes close to max file size
    -	// do not get cut off.
    -	r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead)
    +	var maxBytes int64
    +	if h.FileAPI {
    +		// We add a buffer of bytes.MinRead so that file sizes close to max file size
    +		// do not get cut off.
    +		maxBytes = *c.App.Config().FileSettings.MaxFileSize + bytes.MinRead
    +	} else {
    +		maxBytes = *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes + bytes.MinRead
    +	}
    +
    +	r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
     
     	subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
     	siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath
    
  • server/i18n/en.json+4 0 modified
    @@ -6899,6 +6899,10 @@
         "id": "app.webhooks.update_outgoing.app_error",
         "translation": "Unable to update the webhook."
       },
    +  {
    +    "id": "basic_security_check.url.too_long_error",
    +    "translation": "URL is too long"
    +  },
       {
         "id": "bleveengine.already_started.error",
         "translation": "Bleve is already started."
    
  • server/public/model/utils.go+36 3 modified
    @@ -528,6 +528,40 @@ func ArrayFromJSON(data io.Reader) []string {
     	return objmap
     }
     
    +func SortedArrayFromJSON(data io.Reader) ([]string, error) {
    +	var obj []string
    +	err := json.NewDecoder(data).Decode(&obj)
    +	if err != nil || obj == nil {
    +		return nil, err
    +	}
    +
    +	// Remove duplicate IDs as it can bring a significant load to the database.
    +	return RemoveDuplicateStrings(obj), nil
    +}
    +
    +func NonSortedArrayFromJSON(data io.Reader) ([]string, error) {
    +	var obj []string
    +	err := json.NewDecoder(data).Decode(&obj)
    +	if err != nil || obj == nil {
    +		return nil, err
    +	}
    +
    +	// Remove duplicate IDs, but don't sort.
    +	return RemoveDuplicateStringsNonSort(obj), nil
    +}
    +
    +func RemoveDuplicateStringsNonSort(in []string) []string {
    +	allKeys := make(map[string]bool)
    +	list := []string{}
    +	for _, item := range in {
    +		if _, value := allKeys[item]; !value {
    +			allKeys[item] = true
    +			list = append(list, item)
    +		}
    +	}
    +	return list
    +}
    +
     func ArrayFromInterface(data any) []string {
     	stringArray := []string{}
     
    @@ -561,9 +595,8 @@ func StringInterfaceFromJSON(data io.Reader) map[string]any {
     	return objmap
     }
     
    -func StructFromJSONLimited[V any](data io.Reader, maxBytes int64, obj *V) error {
    -	lr := io.LimitReader(data, maxBytes)
    -	err := json.NewDecoder(lr).Decode(&obj)
    +func StructFromJSONLimited[V any](data io.Reader, obj *V) error {
    +	err := json.NewDecoder(data).Decode(&obj)
     	if err != nil || obj == nil {
     		return err
     	}
    
  • server/public/model/utils_test.go+63 70 modified
    @@ -8,7 +8,6 @@ import (
     	"encoding/json"
     	"errors"
     	"fmt"
    -	"io"
     	"net/http"
     	"reflect"
     	"strings"
    @@ -234,6 +233,66 @@ func TestMapJson(t *testing.T) {
     	require.LessOrEqual(t, len(rm2), 0, "make should be invalid")
     }
     
    +func TestSortedArrayFromJSON(t *testing.T) {
    +	t.Run("Successful parse", func(t *testing.T) {
    +		ids := []string{NewId(), NewId(), NewId()}
    +		b, _ := json.Marshal(ids)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
    +		require.NoError(t, err)
    +		require.ElementsMatch(t, ids, a)
    +	})
    +
    +	t.Run("Empty Array", func(t *testing.T) {
    +		ids := []string{}
    +		b, _ := json.Marshal(ids)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
    +		require.NoError(t, err)
    +		require.Empty(t, a)
    +	})
    +
    +	t.Run("Duplicate keys, returns one", func(t *testing.T) {
    +		var ids []string
    +		id := NewId()
    +		for i := 0; i < 10; i++ {
    +			ids = append(ids, id)
    +		}
    +		b, _ := json.Marshal(ids)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
    +		require.NoError(t, err)
    +		require.Len(t, a, 1)
    +	})
    +}
    +
    +func TestNonSortedArrayFromJSON(t *testing.T) {
    +	t.Run("Successful parse", func(t *testing.T) {
    +		ids := []string{NewId(), NewId(), NewId()}
    +		b, _ := json.Marshal(ids)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
    +		require.NoError(t, err)
    +		require.Equal(t, ids, a)
    +	})
    +
    +	t.Run("Empty Array", func(t *testing.T) {
    +		ids := []string{}
    +		b, _ := json.Marshal(ids)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
    +		require.NoError(t, err)
    +		require.Empty(t, a)
    +	})
    +
    +	t.Run("Duplicate keys, returns one", func(t *testing.T) {
    +		var ids []string
    +		id := NewId()
    +		for i := 0; i <= 10; i++ {
    +			ids = append(ids, id)
    +		}
    +		b, _ := json.Marshal(ids)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
    +		require.NoError(t, err)
    +		require.Len(t, a, 1)
    +	})
    +}
    +
     func TestIsValidEmail(t *testing.T) {
     	for _, testCase := range []struct {
     		Input    string
    @@ -1177,7 +1236,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), b)
     		require.NoError(t, err)
     
     		require.Equal(t, b.StringField, "string")
    @@ -1186,29 +1245,6 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.Equal(t, b.BoolField, true)
     	})
     
    -	t.Run("error too big", func(t *testing.T) {
    -		type TestStruct struct {
    -			StringField string
    -			IntField    int
    -			FloatField  float32
    -			BoolField   bool
    -		}
    -
    -		testStruct := TestStruct{
    -			StringField: "string",
    -			IntField:    2,
    -			FloatField:  3.1415,
    -			BoolField:   true,
    -		}
    -		testStructBytes, err := json.Marshal(testStruct)
    -		require.NoError(t, err)
    -
    -		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 10, b)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("successfully parses nested struct", func(t *testing.T) {
     		type TestStruct struct {
     			StringField string
    @@ -1247,7 +1283,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &NestedStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), b)
     		require.NoError(t, err)
     
     		require.Equal(t, b.FieldA.StringField, "string A")
    @@ -1263,49 +1299,6 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.Equal(t, b.FieldC, []int{5, 9, 1, 5, 7})
     	})
     
    -	t.Run("errors on too big nested struct", func(t *testing.T) {
    -		type TestStruct struct {
    -			StringField string
    -			IntField    int
    -			FloatField  float32
    -			BoolField   bool
    -		}
    -
    -		type NestedStruct struct {
    -			FieldA TestStruct
    -			FieldB TestStruct
    -			FieldC []int
    -		}
    -
    -		testStructA := TestStruct{
    -			StringField: "string A",
    -			IntField:    2,
    -			FloatField:  3.1415,
    -			BoolField:   true,
    -		}
    -
    -		testStructB := TestStruct{
    -			StringField: "string B",
    -			IntField:    3,
    -			FloatField:  100,
    -			BoolField:   false,
    -		}
    -
    -		nestedStruct := NestedStruct{
    -			FieldA: testStructA,
    -			FieldB: testStructB,
    -			FieldC: []int{5, 9, 1, 5, 7},
    -		}
    -
    -		nestedStructBytes, err := json.Marshal(nestedStruct)
    -		require.NoError(t, err)
    -
    -		b := &NestedStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), 50, b)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("handles empty structs", func(t *testing.T) {
     		type TestStruct struct{}
     
    @@ -1314,7 +1307,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), b)
     		require.NoError(t, err)
     	})
     }
    
f6d320017549

Cherrypicking API Handler params for 9.5 (#26563)

https://github.com/mattermost/mattermostHarshil SharmaMar 22, 2024via ghsa
29 files changed · +133 167
  • server/channels/api4/brand.go+1 1 modified
    @@ -13,7 +13,7 @@ import (
     
     func (api *API) InitBrand() {
     	api.BaseRoutes.Brand.Handle("/image", api.APIHandlerTrustRequester(getBrandImage)).Methods("GET")
    -	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage)).Methods("POST")
    +	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(deleteBrandImage)).Methods("DELETE")
     }
     
    
  • server/channels/api4/channel_category.go+1 1 modified
    @@ -118,7 +118,7 @@ func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *htt
     	auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail)
     	defer c.LogAuditRec(auditRec)
     
    -	categoryOrder, err := model.NonSortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	categoryOrder, err := model.NonSortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("updateCategoryOrderForTeamForUser", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/channel.go+6 6 modified
    @@ -425,7 +425,7 @@ func restoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds, err := model.NonSortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.NonSortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("createDirectChannel", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -520,7 +520,7 @@ func searchGroupChannels(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func createGroupChannel(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("createGroupChannel", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -708,7 +708,7 @@ func getChannelsMemberCount(c *Context, w http.ResponseWriter, r *http.Request)
     		return
     	}
     
    -	channelIDs, sortErr := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIDs, sortErr := model.SortedArrayFromJSON(r.Body)
     	if sortErr != nil {
     		c.Err = model.NewAppError("getChannelsMemberCount", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(sortErr)
     		return
    @@ -914,7 +914,7 @@ func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Re
     		return
     	}
     
    -	channelIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getPublicChannelsByIdsForTeam", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1457,7 +1457,7 @@ func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request)
     		return
     	}
     
    -	userIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getChannelMembersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1583,7 +1583,7 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
     func readMultipleChannels(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequireUserId()
     
    -	channelIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("readMultipleChannels", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/data_retention.go+4 4 modified
    @@ -264,7 +264,7 @@ func searchTeamsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	teamIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	teamIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("addTeamsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -291,7 +291,7 @@ func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	teamIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	teamIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("removeTeamsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -383,7 +383,7 @@ func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request)
     func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	channelIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("addChannelsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -411,7 +411,7 @@ func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	channelIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("removeChannelsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/emoji.go+2 2 modified
    @@ -21,7 +21,7 @@ const (
     )
     
     func (api *API) InitEmoji() {
    -	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji)).Methods("POST")
    +	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods("GET")
     	api.BaseRoutes.Emojis.Handle("/names", api.APISessionRequired(getEmojisByNames)).Methods("POST")
     	api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods("POST")
    @@ -240,7 +240,7 @@ func getEmojiByName(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getEmojisByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	names, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	names, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getEmojisByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/file.go+1 1 modified
    @@ -32,7 +32,7 @@ const (
     const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
     
     func (api *API) InitFile() {
    -	api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST")
    +	api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET")
     	api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
     	api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET")
    
  • server/channels/api4/handlers.go+45 9 modified
    @@ -18,9 +18,15 @@ type Context = web.Context
     
     type handlerFunc func(*Context, http.ResponseWriter, *http.Request)
     
    +type APIHandlerOption string
    +
    +const (
    +	handlerParamFileAPI = APIHandlerOption("fileAPI")
    +)
    +
     // APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
     // granted.
    -func (api *API) APIHandler(h handlerFunc) http.Handler {
    +func (api *API) APIHandler(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -31,6 +37,8 @@ func (api *API) APIHandler(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -39,7 +47,7 @@ func (api *API) APIHandler(h handlerFunc) http.Handler {
     
     // APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
     // be granted.
    -func (api *API) APISessionRequired(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -50,14 +58,16 @@ func (api *API) APISessionRequired(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
     	return handler
     }
     
     // CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
    -func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
    +func (api *API) CloudAPIKeyRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:             api.srv,
     		HandleFunc:      h,
    @@ -69,14 +79,16 @@ func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
     		IsStatic:        false,
     		IsLocal:         false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
     	return handler
     }
     
     // RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints.
    -func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
    +func (api *API) RemoteClusterTokenRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:                       api.srv,
     		HandleFunc:                h,
    @@ -89,6 +101,8 @@ func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
     		IsStatic:                  false,
     		IsLocal:                   false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -98,7 +112,7 @@ func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
     // APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session  but when accessed,
     // if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
     // authentication must be waived.
    -func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredMfa(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -109,6 +123,8 @@ func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -118,7 +134,7 @@ func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
     // APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
     // allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
     // websocket.
    -func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
    +func (api *API) APIHandlerTrustRequester(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -129,6 +145,8 @@ func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -137,7 +155,7 @@ func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
     
     // APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and
     // are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads.
    -func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredTrustRequester(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -148,6 +166,8 @@ func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -156,7 +176,7 @@ func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
     
     // DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load,
     // responding with HTTP 503 (Service Unavailable).
    -func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:             api.srv,
     		HandleFunc:      h,
    @@ -168,6 +188,8 @@ func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
     		IsLocal:         false,
     		DisableWhenBusy: true,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -178,7 +200,7 @@ func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
     // mode, this is, through a UNIX socket and without an authenticated
     // session, but with one that has no user set and no permission
     // restrictions
    -func (api *API) APILocal(h handlerFunc) http.Handler {
    +func (api *API) APILocal(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -189,6 +211,7 @@ func (api *API) APILocal(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        true,
     	}
    +	setHandlerOpts(handler, opts...)
     
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
    @@ -223,3 +246,16 @@ func minimumProfessionalLicense(c *Context) *model.AppError {
     	}
     	return nil
     }
    +
    +func setHandlerOpts(handler *web.Handler, opts ...APIHandlerOption) {
    +	if len(opts) == 0 {
    +		return
    +	}
    +
    +	for _, option := range opts {
    +		switch option {
    +		case handlerParamFileAPI:
    +			handler.FileAPI = true
    +		}
    +	}
    +}
    
  • server/channels/api4/ldap.go+2 2 modified
    @@ -34,8 +34,8 @@ func (api *API) InitLdap() {
     	// DELETE /api/v4/ldap/groups/:remote_id/link
     	api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods("DELETE")
     
    -	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate)).Methods("POST")
    -	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate)).Methods("POST")
    +	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate, handlerParamFileAPI)).Methods("POST")
    +	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate, handlerParamFileAPI)).Methods("POST")
     
     	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
     	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
    
  • server/channels/api4/license.go+1 1 modified
    @@ -21,7 +21,7 @@ import (
     func (api *API) InitLicense() {
     	api.BaseRoutes.APIRoot.Handle("/trial-license", api.APISessionRequired(requestTrialLicense)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET")
    -	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense)).Methods("POST")
    +	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
     	api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
     	api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
    
  • server/channels/api4/license_local.go+1 1 modified
    @@ -15,7 +15,7 @@ import (
     )
     
     func (api *API) InitLicenseLocal() {
    -	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense)).Methods("POST")
    +	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localRemoveLicense)).Methods("DELETE")
     }
     
    
  • server/channels/api4/plugin.go+1 1 modified
    @@ -24,7 +24,7 @@ const (
     )
     
     func (api *API) InitPlugin() {
    -	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin)).Methods("POST")
    +	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(getPlugins)).Methods("GET")
     	api.BaseRoutes.Plugin.Handle("", api.APISessionRequired(removePlugin)).Methods("DELETE")
     	api.BaseRoutes.Plugins.Handle("/install_from_url", api.APISessionRequired(installPluginFromURL)).Methods("POST")
    
  • server/channels/api4/plugin_local.go+1 1 modified
    @@ -4,7 +4,7 @@
     package api4
     
     func (api *API) InitPluginLocal() {
    -	api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin)).Methods("POST")
    +	api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Plugins.Handle("", api.APILocal(getPlugins)).Methods("GET")
     	api.BaseRoutes.Plugins.Handle("/install_from_url", api.APILocal(installPluginFromURL)).Methods("POST")
     	api.BaseRoutes.Plugin.Handle("", api.APILocal(removePlugin)).Methods("DELETE")
    
  • server/channels/api4/post.go+1 1 modified
    @@ -504,7 +504,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
     
     // getPostsByIds also sets a header to indicate, if posts were truncated as per the cloud plan's limit.
     func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	postIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	postIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getPostsByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/preference.go+2 2 modified
    @@ -103,7 +103,7 @@ func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	var preferences model.Preferences
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, &preferences)
    +	err := model.StructFromJSONLimited(r.Body, &preferences)
     	if err != nil {
     		c.SetInvalidParamWithErr("preferences", err)
     		return
    @@ -155,7 +155,7 @@ func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	var preferences model.Preferences
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, &preferences)
    +	err := model.StructFromJSONLimited(r.Body, &preferences)
     	if err != nil {
     		c.SetInvalidParamWithErr("preferences", err)
     		return
    
  • server/channels/api4/reaction.go+1 1 modified
    @@ -119,7 +119,7 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
    -	postIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	postIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getBulkReactions", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/remote_cluster.go+2 2 modified
    @@ -20,8 +20,8 @@ func (api *API) InitRemoteCluster() {
     	api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods("POST")
     	api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods("POST")
     	api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
    -	api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData)).Methods("POST")
    -	api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage)).Methods("POST")
    +	api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData, handlerParamFileAPI)).Methods("POST")
    +	api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage, handlerParamFileAPI)).Methods("POST")
     }
     
     func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
    
  • server/channels/api4/role.go+1 1 modified
    @@ -84,7 +84,7 @@ func getRoleByName(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getRolesByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	rolenames, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	rolenames, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getRolesByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/status.go+1 1 modified
    @@ -49,7 +49,7 @@ func getUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUserStatusesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getUserStatusesByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/system.go+1 1 modified
    @@ -870,7 +870,7 @@ func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Reque
     	defer c.LogAuditRec(auditRec)
     	c.LogAudit("attempt")
     
    -	ids, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	ids, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("updateViewedProductNotices", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/team.go+3 3 modified
    @@ -49,7 +49,7 @@ func (api *API) InitTeam() {
     	api.BaseRoutes.Team.Handle("/regenerate_invite_id", api.APISessionRequired(regenerateTeamInviteId)).Methods("POST")
     
     	api.BaseRoutes.Team.Handle("/image", api.APISessionRequiredTrustRequester(getTeamIcon)).Methods("GET")
    -	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon)).Methods("POST")
    +	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(removeTeamIcon)).Methods("DELETE")
     
     	api.BaseRoutes.TeamMembers.Handle("", api.APISessionRequired(getTeamMembers)).Methods("GET")
    @@ -644,7 +644,7 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	userIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getTeamMembersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1376,7 +1376,7 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	memberInvite := &model.MemberInvite{}
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, memberInvite)
    +	err := model.StructFromJSONLimited(r.Body, memberInvite)
     	if err != nil {
     		c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/team_local.go+1 1 modified
    @@ -79,7 +79,7 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
     	}
     
     	memberInvite := &model.MemberInvite{}
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, memberInvite)
    +	err := model.StructFromJSONLimited(r.Body, memberInvite)
     	if err != nil {
     		c.Err = model.NewAppError("Api4.localInviteUsersToTeam", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/upload.go+2 2 modified
    @@ -17,9 +17,9 @@ import (
     )
     
     func (api *API) InitUpload() {
    -	api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload)).Methods("POST")
    +	api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(getUpload)).Methods("GET")
    -	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData)).Methods("POST")
    +	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData, handlerParamFileAPI)).Methods("POST")
     }
     
     func createUpload(c *Context, w http.ResponseWriter, r *http.Request) {
    
  • server/channels/api4/upload_local.go+2 2 modified
    @@ -4,7 +4,7 @@
     package api4
     
     func (api *API) InitUploadLocal() {
    -	api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload)).Methods("POST")
    +	api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Upload.Handle("", api.APILocal(getUpload)).Methods("GET")
    -	api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData)).Methods("POST")
    +	api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData, handlerParamFileAPI)).Methods("POST")
     }
    
  • server/channels/api4/user.go+4 4 modified
    @@ -38,7 +38,7 @@ func (api *API) InitUser() {
     	api.BaseRoutes.User.Handle("", api.APISessionRequired(getUser)).Methods("GET")
     	api.BaseRoutes.User.Handle("/image/default", api.APISessionRequiredTrustRequester(getDefaultProfileImage)).Methods("GET")
     	api.BaseRoutes.User.Handle("/image", api.APISessionRequiredTrustRequester(getProfileImage)).Methods("GET")
    -	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage)).Methods("POST")
    +	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setDefaultProfileImage)).Methods("DELETE")
     	api.BaseRoutes.User.Handle("", api.APISessionRequired(updateUser)).Methods("PUT")
     	api.BaseRoutes.User.Handle("/patch", api.APISessionRequired(patchUser)).Methods("PUT")
    @@ -623,7 +623,7 @@ func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	channelIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil || len(channelIds) == 0 {
     		c.Err = model.NewAppError("getUsersByGroupChannelIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -955,7 +955,7 @@ func requireGroupAccess(c *web.Context, groupID string) *model.AppError {
     }
     
     func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getUsersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1002,7 +1002,7 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	usernames, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	usernames, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getUsersByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/user_local.go+1 1 modified
    @@ -233,7 +233,7 @@ func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func localGetUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("localGetUsersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/web/handlers.go+26 8 modified
    @@ -31,7 +31,8 @@ import (
     )
     
     const (
    -	frameAncestors = "'self' teams.microsoft.com"
    +	frameAncestors   = "'self' teams.microsoft.com"
    +	maxURLCharacters = 2048
     )
     
     func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
    @@ -86,6 +87,7 @@ type Handler struct {
     	IsStatic                  bool
     	IsLocal                   bool
     	DisableWhenBusy           bool
    +	FileAPI                   bool
     
     	cspShaDirective string
     }
    @@ -142,7 +144,20 @@ func generateDevCSP(c Context) string {
     	return " " + strings.Join(devCSP, " ")
     }
     
    +func (h Handler) basicSecurityChecks(w http.ResponseWriter, r *http.Request) *model.AppError {
    +	if len(r.RequestURI) > maxURLCharacters {
    +		return model.NewAppError("basicSecurityChecks", "basic_security_check.url.too_long_error", nil, "", http.StatusRequestURITooLong)
    +	}
    +
    +	return nil
    +}
    +
     func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    +	if appErr := h.basicSecurityChecks(w, r); appErr != nil {
    +		http.Error(w, appErr.Error(), appErr.StatusCode)
    +		return
    +	}
    +
     	w = newWrappedWriter(w)
     	now := time.Now()
     
    @@ -213,13 +228,16 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     		c.App = app_opentracing.NewOpenTracingAppLayer(c.App, ctx)
     	}
     
    -	// Set the max request body size to be equal to MaxFileSize.
    -	// Ideally, non-file request bodies should be smaller than file request bodies,
    -	// but we don't have a clean way to identify all file upload handlers.
    -	// So to keep it simple, we clamp it to the max file size.
    -	// We add a buffer of bytes.MinRead so that file sizes close to max file size
    -	// do not get cut off.
    -	r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead)
    +	var maxBytes int64
    +	if h.FileAPI {
    +		// We add a buffer of bytes.MinRead so that file sizes close to max file size
    +		// do not get cut off.
    +		maxBytes = *c.App.Config().FileSettings.MaxFileSize + bytes.MinRead
    +	} else {
    +		maxBytes = *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes + bytes.MinRead
    +	}
    +
    +	r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
     
     	subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
     	siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath
    
  • server/i18n/en.json+4 0 modified
    @@ -7158,6 +7158,10 @@
         "id": "app.webhooks.update_outgoing.app_error",
         "translation": "Unable to update the webhook."
       },
    +  {
    +    "id": "basic_security_check.url.too_long_error",
    +    "translation": "URL is too long"
    +  },
       {
         "id": "bleveengine.already_started.error",
         "translation": "Bleve is already started."
    
  • server/public/model/utils.go+6 9 modified
    @@ -501,10 +501,9 @@ func ArrayFromJSON(data io.Reader) []string {
     	return objmap
     }
     
    -func SortedArrayFromJSON(data io.Reader, maxBytes int64) ([]string, error) {
    +func SortedArrayFromJSON(data io.Reader) ([]string, error) {
     	var obj []string
    -	lr := io.LimitReader(data, maxBytes)
    -	err := json.NewDecoder(lr).Decode(&obj)
    +	err := json.NewDecoder(data).Decode(&obj)
     	if err != nil || obj == nil {
     		return nil, err
     	}
    @@ -513,10 +512,9 @@ func SortedArrayFromJSON(data io.Reader, maxBytes int64) ([]string, error) {
     	return RemoveDuplicateStrings(obj), nil
     }
     
    -func NonSortedArrayFromJSON(data io.Reader, maxBytes int64) ([]string, error) {
    +func NonSortedArrayFromJSON(data io.Reader) ([]string, error) {
     	var obj []string
    -	lr := io.LimitReader(data, maxBytes)
    -	err := json.NewDecoder(lr).Decode(&obj)
    +	err := json.NewDecoder(data).Decode(&obj)
     	if err != nil || obj == nil {
     		return nil, err
     	}
    @@ -558,9 +556,8 @@ func StringInterfaceFromJSON(data io.Reader) map[string]any {
     	return objmap
     }
     
    -func StructFromJSONLimited[V any](data io.Reader, maxBytes int64, obj *V) error {
    -	lr := io.LimitReader(data, maxBytes)
    -	err := json.NewDecoder(lr).Decode(&obj)
    +func StructFromJSONLimited[V any](data io.Reader, obj *V) error {
    +	err := json.NewDecoder(data).Decode(&obj)
     	if err != nil || obj == nil {
     		return err
     	}
    
  • server/public/model/utils_test.go+9 98 modified
    @@ -8,7 +8,6 @@ import (
     	"encoding/json"
     	"errors"
     	"fmt"
    -	"io"
     	"net/http"
     	"reflect"
     	"strings"
    @@ -242,38 +241,27 @@ func TestSortedArrayFromJSON(t *testing.T) {
     	t.Run("Successful parse", func(t *testing.T) {
     		ids := []string{NewId(), NewId(), NewId()}
     		b, _ := json.Marshal(ids)
    -		a, err := SortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.ElementsMatch(t, ids, a)
     	})
     
     	t.Run("Empty Array", func(t *testing.T) {
     		ids := []string{}
     		b, _ := json.Marshal(ids)
    -		a, err := SortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Empty(t, a)
     	})
     
    -	t.Run("Error too large", func(t *testing.T) {
    -		var ids []string
    -		for i := 0; i <= 100; i++ {
    -			ids = append(ids, NewId())
    -		}
    -		b, _ := json.Marshal(ids)
    -		_, err := SortedArrayFromJSON(bytes.NewReader(b), 1000)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("Duplicate keys, returns one", func(t *testing.T) {
     		var ids []string
     		id := NewId()
     		for i := 0; i < 10; i++ {
     			ids = append(ids, id)
     		}
     		b, _ := json.Marshal(ids)
    -		a, err := SortedArrayFromJSON(bytes.NewReader(b), 26000)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Len(t, a, 1)
     	})
    @@ -283,38 +271,27 @@ func TestNonSortedArrayFromJSON(t *testing.T) {
     	t.Run("Successful parse", func(t *testing.T) {
     		ids := []string{NewId(), NewId(), NewId()}
     		b, _ := json.Marshal(ids)
    -		a, err := NonSortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Equal(t, ids, a)
     	})
     
     	t.Run("Empty Array", func(t *testing.T) {
     		ids := []string{}
     		b, _ := json.Marshal(ids)
    -		a, err := NonSortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Empty(t, a)
     	})
     
    -	t.Run("Error too large", func(t *testing.T) {
    -		var ids []string
    -		for i := 0; i <= 100; i++ {
    -			ids = append(ids, NewId())
    -		}
    -		b, _ := json.Marshal(ids)
    -		_, err := NonSortedArrayFromJSON(bytes.NewReader(b), 1000)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("Duplicate keys, returns one", func(t *testing.T) {
     		var ids []string
     		id := NewId()
     		for i := 0; i <= 10; i++ {
     			ids = append(ids, id)
     		}
     		b, _ := json.Marshal(ids)
    -		a, err := NonSortedArrayFromJSON(bytes.NewReader(b), 26000)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Len(t, a, 1)
     	})
    @@ -1263,7 +1240,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), b)
     		require.NoError(t, err)
     
     		require.Equal(t, b.StringField, "string")
    @@ -1272,29 +1249,6 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.Equal(t, b.BoolField, true)
     	})
     
    -	t.Run("error too big", func(t *testing.T) {
    -		type TestStruct struct {
    -			StringField string
    -			IntField    int
    -			FloatField  float32
    -			BoolField   bool
    -		}
    -
    -		testStruct := TestStruct{
    -			StringField: "string",
    -			IntField:    2,
    -			FloatField:  3.1415,
    -			BoolField:   true,
    -		}
    -		testStructBytes, err := json.Marshal(testStruct)
    -		require.NoError(t, err)
    -
    -		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 10, b)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("successfully parses nested struct", func(t *testing.T) {
     		type TestStruct struct {
     			StringField string
    @@ -1333,7 +1287,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &NestedStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), b)
     		require.NoError(t, err)
     
     		require.Equal(t, b.FieldA.StringField, "string A")
    @@ -1349,49 +1303,6 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.Equal(t, b.FieldC, []int{5, 9, 1, 5, 7})
     	})
     
    -	t.Run("errors on too big nested struct", func(t *testing.T) {
    -		type TestStruct struct {
    -			StringField string
    -			IntField    int
    -			FloatField  float32
    -			BoolField   bool
    -		}
    -
    -		type NestedStruct struct {
    -			FieldA TestStruct
    -			FieldB TestStruct
    -			FieldC []int
    -		}
    -
    -		testStructA := TestStruct{
    -			StringField: "string A",
    -			IntField:    2,
    -			FloatField:  3.1415,
    -			BoolField:   true,
    -		}
    -
    -		testStructB := TestStruct{
    -			StringField: "string B",
    -			IntField:    3,
    -			FloatField:  100,
    -			BoolField:   false,
    -		}
    -
    -		nestedStruct := NestedStruct{
    -			FieldA: testStructA,
    -			FieldB: testStructB,
    -			FieldC: []int{5, 9, 1, 5, 7},
    -		}
    -
    -		nestedStructBytes, err := json.Marshal(nestedStruct)
    -		require.NoError(t, err)
    -
    -		b := &NestedStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), 50, b)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("handles empty structs", func(t *testing.T) {
     		type TestStruct struct{}
     
    @@ -1400,7 +1311,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), b)
     		require.NoError(t, err)
     	})
     }
    
13049d8b16b1

Manual cherrypick for API handler options for v9.6 (#26560)

https://github.com/mattermost/mattermostHarshil SharmaMar 22, 2024via ghsa
7 files changed · +9 9
  • server/channels/api4/emoji.go+1 1 modified
    @@ -21,7 +21,7 @@ const (
     )
     
     func (api *API) InitEmoji() {
    -	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji)).Methods("POST")
    +	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(createEmoji, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Emojis.Handle("", api.APISessionRequired(getEmojiList)).Methods("GET")
     	api.BaseRoutes.Emojis.Handle("/names", api.APISessionRequired(getEmojisByNames)).Methods("POST")
     	api.BaseRoutes.Emojis.Handle("/search", api.APISessionRequired(searchEmojis)).Methods("POST")
    
  • server/channels/api4/ldap.go+2 2 modified
    @@ -34,8 +34,8 @@ func (api *API) InitLdap() {
     	// DELETE /api/v4/ldap/groups/:remote_id/link
     	api.BaseRoutes.LDAP.Handle(`/groups/{remote_id}/link`, api.APISessionRequired(unlinkLdapGroup)).Methods("DELETE")
     
    -	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate)).Methods("POST")
    -	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate)).Methods("POST")
    +	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(addLdapPublicCertificate, handlerParamFileAPI)).Methods("POST")
    +	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(addLdapPrivateCertificate, handlerParamFileAPI)).Methods("POST")
     
     	api.BaseRoutes.LDAP.Handle("/certificate/public", api.APISessionRequired(removeLdapPublicCertificate)).Methods("DELETE")
     	api.BaseRoutes.LDAP.Handle("/certificate/private", api.APISessionRequired(removeLdapPrivateCertificate)).Methods("DELETE")
    
  • server/channels/api4/license.go+1 1 modified
    @@ -21,7 +21,7 @@ import (
     func (api *API) InitLicense() {
     	api.BaseRoutes.APIRoot.Handle("/trial-license", api.APISessionRequired(requestTrialLicense)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/trial-license/prev", api.APISessionRequired(getPrevTrialLicense)).Methods("GET")
    -	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense)).Methods("POST")
    +	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(addLicense, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/license", api.APISessionRequired(removeLicense)).Methods("DELETE")
     	api.BaseRoutes.APIRoot.Handle("/license/renewal", api.APISessionRequired(requestRenewalLink)).Methods("GET")
     	api.BaseRoutes.APIRoot.Handle("/license/client", api.APIHandler(getClientLicense)).Methods("GET")
    
  • server/channels/api4/license_local.go+1 1 modified
    @@ -15,7 +15,7 @@ import (
     )
     
     func (api *API) InitLicenseLocal() {
    -	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense)).Methods("POST")
    +	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localAddLicense, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.APIRoot.Handle("/license", api.APILocal(localRemoveLicense)).Methods("DELETE")
     }
     
    
  • server/channels/api4/plugin.go+1 1 modified
    @@ -24,7 +24,7 @@ const (
     )
     
     func (api *API) InitPlugin() {
    -	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin)).Methods("POST")
    +	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(uploadPlugin, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Plugins.Handle("", api.APISessionRequired(getPlugins)).Methods("GET")
     	api.BaseRoutes.Plugin.Handle("", api.APISessionRequired(removePlugin)).Methods("DELETE")
     	api.BaseRoutes.Plugins.Handle("/install_from_url", api.APISessionRequired(installPluginFromURL)).Methods("POST")
    
  • server/channels/api4/plugin_local.go+1 1 modified
    @@ -4,7 +4,7 @@
     package api4
     
     func (api *API) InitPluginLocal() {
    -	api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin)).Methods("POST")
    +	api.BaseRoutes.Plugins.Handle("", api.APILocal(uploadPlugin, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Plugins.Handle("", api.APILocal(getPlugins)).Methods("GET")
     	api.BaseRoutes.Plugins.Handle("/install_from_url", api.APILocal(installPluginFromURL)).Methods("POST")
     	api.BaseRoutes.Plugin.Handle("", api.APILocal(removePlugin)).Methods("DELETE")
    
  • server/channels/api4/remote_cluster.go+2 2 modified
    @@ -20,8 +20,8 @@ func (api *API) InitRemoteCluster() {
     	api.BaseRoutes.RemoteCluster.Handle("/ping", api.RemoteClusterTokenRequired(remoteClusterPing)).Methods("POST")
     	api.BaseRoutes.RemoteCluster.Handle("/msg", api.RemoteClusterTokenRequired(remoteClusterAcceptMessage)).Methods("POST")
     	api.BaseRoutes.RemoteCluster.Handle("/confirm_invite", api.RemoteClusterTokenRequired(remoteClusterConfirmInvite)).Methods("POST")
    -	api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData)).Methods("POST")
    -	api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage)).Methods("POST")
    +	api.BaseRoutes.RemoteCluster.Handle("/upload/{upload_id:[A-Za-z0-9]+}", api.RemoteClusterTokenRequired(uploadRemoteData, handlerParamFileAPI)).Methods("POST")
    +	api.BaseRoutes.RemoteCluster.Handle("/{user_id:[A-Za-z0-9]+}/image", api.RemoteClusterTokenRequired(remoteSetProfileImage, handlerParamFileAPI)).Methods("POST")
     }
     
     func remoteClusterPing(c *Context, w http.ResponseWriter, r *http.Request) {
    
54478f2ccbc6

API handler opts modifier (#26148) (#26550)

https://github.com/mattermost/mattermostMattermost BuildMar 21, 2024via ghsa
24 files changed · +127 165
  • server/channels/api4/brand.go+1 1 modified
    @@ -13,7 +13,7 @@ import (
     
     func (api *API) InitBrand() {
     	api.BaseRoutes.Brand.Handle("/image", api.APIHandlerTrustRequester(getBrandImage)).Methods("GET")
    -	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage)).Methods("POST")
    +	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(uploadBrandImage, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Brand.Handle("/image", api.APISessionRequired(deleteBrandImage)).Methods("DELETE")
     }
     
    
  • server/channels/api4/channel_category.go+1 1 modified
    @@ -118,7 +118,7 @@ func updateCategoryOrderForTeamForUser(c *Context, w http.ResponseWriter, r *htt
     	auditRec := c.MakeAuditRecord("updateCategoryOrderForTeamForUser", audit.Fail)
     	defer c.LogAuditRec(auditRec)
     
    -	categoryOrder, err := model.NonSortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	categoryOrder, err := model.NonSortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("updateCategoryOrderForTeamForUser", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/channel.go+6 6 modified
    @@ -425,7 +425,7 @@ func restoreChannel(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds, err := model.NonSortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.NonSortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("createDirectChannel", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -520,7 +520,7 @@ func searchGroupChannels(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func createGroupChannel(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("createGroupChannel", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -708,7 +708,7 @@ func getChannelsMemberCount(c *Context, w http.ResponseWriter, r *http.Request)
     		return
     	}
     
    -	channelIDs, sortErr := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIDs, sortErr := model.SortedArrayFromJSON(r.Body)
     	if sortErr != nil {
     		c.Err = model.NewAppError("getChannelsMemberCount", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(sortErr)
     		return
    @@ -914,7 +914,7 @@ func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Re
     		return
     	}
     
    -	channelIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getPublicChannelsByIdsForTeam", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1457,7 +1457,7 @@ func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request)
     		return
     	}
     
    -	userIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getChannelMembersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1583,7 +1583,7 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
     func readMultipleChannels(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequireUserId()
     
    -	channelIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("readMultipleChannels", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/data_retention.go+4 4 modified
    @@ -264,7 +264,7 @@ func searchTeamsInPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	teamIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	teamIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("addTeamsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -291,7 +291,7 @@ func addTeamsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func removeTeamsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	teamIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	teamIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("removeTeamsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -383,7 +383,7 @@ func searchChannelsInPolicy(c *Context, w http.ResponseWriter, r *http.Request)
     func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	channelIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("addChannelsToPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -411,7 +411,7 @@ func addChannelsToPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     func removeChannelsFromPolicy(c *Context, w http.ResponseWriter, r *http.Request) {
     	c.RequirePolicyId()
     	policyId := c.Params.PolicyId
    -	channelIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("removeChannelsFromPolicy", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/emoji.go+1 1 modified
    @@ -240,7 +240,7 @@ func getEmojiByName(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getEmojisByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	names, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	names, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getEmojisByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/file.go+1 1 modified
    @@ -32,7 +32,7 @@ const (
     const maxMultipartFormDataBytes = 10 * 1024 // 10Kb
     
     func (api *API) InitFile() {
    -	api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream)).Methods("POST")
    +	api.BaseRoutes.Files.Handle("", api.APISessionRequired(uploadFileStream, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.File.Handle("", api.APISessionRequiredTrustRequester(getFile)).Methods("GET")
     	api.BaseRoutes.File.Handle("/thumbnail", api.APISessionRequiredTrustRequester(getFileThumbnail)).Methods("GET")
     	api.BaseRoutes.File.Handle("/link", api.APISessionRequired(getFileLink)).Methods("GET")
    
  • server/channels/api4/handlers.go+45 9 modified
    @@ -18,9 +18,15 @@ type Context = web.Context
     
     type handlerFunc func(*Context, http.ResponseWriter, *http.Request)
     
    +type APIHandlerOption string
    +
    +const (
    +	handlerParamFileAPI = APIHandlerOption("fileAPI")
    +)
    +
     // APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
     // granted.
    -func (api *API) APIHandler(h handlerFunc) http.Handler {
    +func (api *API) APIHandler(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -31,6 +37,8 @@ func (api *API) APIHandler(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -39,7 +47,7 @@ func (api *API) APIHandler(h handlerFunc) http.Handler {
     
     // APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
     // be granted.
    -func (api *API) APISessionRequired(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -50,14 +58,16 @@ func (api *API) APISessionRequired(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
     	return handler
     }
     
     // CloudAPIKeyRequired provides a handler for webhook endpoints to access Cloud installations from CWS
    -func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
    +func (api *API) CloudAPIKeyRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:             api.srv,
     		HandleFunc:      h,
    @@ -69,14 +79,16 @@ func (api *API) CloudAPIKeyRequired(h handlerFunc) http.Handler {
     		IsStatic:        false,
     		IsLocal:         false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
     	return handler
     }
     
     // RemoteClusterTokenRequired provides a handler for remote cluster requests to /remotecluster endpoints.
    -func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
    +func (api *API) RemoteClusterTokenRequired(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:                       api.srv,
     		HandleFunc:                h,
    @@ -89,6 +101,8 @@ func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
     		IsStatic:                  false,
     		IsLocal:                   false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -98,7 +112,7 @@ func (api *API) RemoteClusterTokenRequired(h handlerFunc) http.Handler {
     // APISessionRequiredMfa provides a handler for API endpoints which require a logged-in user session  but when accessed,
     // if MFA is enabled, the MFA process is not yet complete, and therefore the requirement to have completed the MFA
     // authentication must be waived.
    -func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredMfa(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -109,6 +123,8 @@ func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -118,7 +134,7 @@ func (api *API) APISessionRequiredMfa(h handlerFunc) http.Handler {
     // APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
     // allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
     // websocket.
    -func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
    +func (api *API) APIHandlerTrustRequester(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -129,6 +145,8 @@ func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -137,7 +155,7 @@ func (api *API) APIHandlerTrustRequester(h handlerFunc) http.Handler {
     
     // APISessionRequiredTrustRequester provides a handler for API endpoints which do require the user to be logged in and
     // are allowed to be requested directly rather than via javascript/XMLHttpRequest, such as emoji or file uploads.
    -func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredTrustRequester(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -148,6 +166,8 @@ func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        false,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -156,7 +176,7 @@ func (api *API) APISessionRequiredTrustRequester(h handlerFunc) http.Handler {
     
     // DisableWhenBusy provides a handler for API endpoints which should be disabled when the server is under load,
     // responding with HTTP 503 (Service Unavailable).
    -func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
    +func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:             api.srv,
     		HandleFunc:      h,
    @@ -168,6 +188,8 @@ func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
     		IsLocal:         false,
     		DisableWhenBusy: true,
     	}
    +	setHandlerOpts(handler, opts...)
    +
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
     	}
    @@ -178,7 +200,7 @@ func (api *API) APISessionRequiredDisableWhenBusy(h handlerFunc) http.Handler {
     // mode, this is, through a UNIX socket and without an authenticated
     // session, but with one that has no user set and no permission
     // restrictions
    -func (api *API) APILocal(h handlerFunc) http.Handler {
    +func (api *API) APILocal(h handlerFunc, opts ...APIHandlerOption) http.Handler {
     	handler := &web.Handler{
     		Srv:            api.srv,
     		HandleFunc:     h,
    @@ -189,6 +211,7 @@ func (api *API) APILocal(h handlerFunc) http.Handler {
     		IsStatic:       false,
     		IsLocal:        true,
     	}
    +	setHandlerOpts(handler, opts...)
     
     	if *api.srv.Config().ServiceSettings.WebserverMode == "gzip" {
     		return gzhttp.GzipHandler(handler)
    @@ -223,3 +246,16 @@ func minimumProfessionalLicense(c *Context) *model.AppError {
     	}
     	return nil
     }
    +
    +func setHandlerOpts(handler *web.Handler, opts ...APIHandlerOption) {
    +	if len(opts) == 0 {
    +		return
    +	}
    +
    +	for _, option := range opts {
    +		switch option {
    +		case handlerParamFileAPI:
    +			handler.FileAPI = true
    +		}
    +	}
    +}
    
  • server/channels/api4/outgoing_oauth_connection.go+3 7 modified
    @@ -6,7 +6,6 @@ package api4
     import (
     	"encoding/json"
     	"fmt"
    -	"io"
     	"net/http"
     	"net/url"
     	"strconv"
    @@ -220,8 +219,7 @@ func createOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Re
     	}
     
     	var inputConnection model.OutgoingOAuthConnection
    -	bodyReader := io.LimitReader(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    -	if err := json.NewDecoder(bodyReader).Decode(&inputConnection); err != nil {
    +	if err := json.NewDecoder(r.Body).Decode(&inputConnection); err != nil {
     		c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.create_connection.input_error", nil, err.Error(), http.StatusBadRequest)
     		return
     	}
    @@ -271,8 +269,7 @@ func updateOutgoingOAuthConnection(c *Context, w http.ResponseWriter, r *http.Re
     	}
     
     	var inputConnection model.OutgoingOAuthConnection
    -	bodyReader := io.LimitReader(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    -	if err := json.NewDecoder(bodyReader).Decode(&inputConnection); err != nil {
    +	if err := json.NewDecoder(r.Body).Decode(&inputConnection); err != nil {
     		c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.update_connection.input_error", nil, err.Error(), http.StatusBadRequest)
     		return
     	}
    @@ -376,8 +373,7 @@ func validateOutgoingOAuthConnectionCredentials(c *Context, w http.ResponseWrite
     	// connection url.
     	var inputConnection *model.OutgoingOAuthConnection
     
    -	bodyReader := io.LimitReader(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    -	if err := json.NewDecoder(bodyReader).Decode(&inputConnection); err != nil {
    +	if err := json.NewDecoder(r.Body).Decode(&inputConnection); err != nil {
     		c.Err = model.NewAppError(whereOutgoingOAuthConnection, "api.context.outgoing_oauth_connection.validate_connection_credentials.input_error", nil, err.Error(), http.StatusBadRequest)
     		w.WriteHeader(c.Err.StatusCode)
     		return
    
  • server/channels/api4/post.go+1 1 modified
    @@ -504,7 +504,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
     
     // getPostsByIds also sets a header to indicate, if posts were truncated as per the cloud plan's limit.
     func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	postIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	postIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getPostsByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/preference.go+2 2 modified
    @@ -103,7 +103,7 @@ func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	var preferences model.Preferences
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, &preferences)
    +	err := model.StructFromJSONLimited(r.Body, &preferences)
     	if err != nil {
     		c.SetInvalidParamWithErr("preferences", err)
     		return
    @@ -155,7 +155,7 @@ func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	var preferences model.Preferences
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, &preferences)
    +	err := model.StructFromJSONLimited(r.Body, &preferences)
     	if err != nil {
     		c.SetInvalidParamWithErr("preferences", err)
     		return
    
  • server/channels/api4/reaction.go+1 1 modified
    @@ -119,7 +119,7 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
    -	postIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	postIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getBulkReactions", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/role.go+1 1 modified
    @@ -84,7 +84,7 @@ func getRoleByName(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getRolesByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	rolenames, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	rolenames, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getRolesByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/status.go+1 1 modified
    @@ -49,7 +49,7 @@ func getUserStatus(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUserStatusesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getUserStatusesByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/system.go+1 1 modified
    @@ -870,7 +870,7 @@ func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Reque
     	defer c.LogAuditRec(auditRec)
     	c.LogAudit("attempt")
     
    -	ids, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	ids, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("updateViewedProductNotices", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/team.go+3 3 modified
    @@ -49,7 +49,7 @@ func (api *API) InitTeam() {
     	api.BaseRoutes.Team.Handle("/regenerate_invite_id", api.APISessionRequired(regenerateTeamInviteId)).Methods("POST")
     
     	api.BaseRoutes.Team.Handle("/image", api.APISessionRequiredTrustRequester(getTeamIcon)).Methods("GET")
    -	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon)).Methods("POST")
    +	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(setTeamIcon, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Team.Handle("/image", api.APISessionRequired(removeTeamIcon)).Methods("DELETE")
     
     	api.BaseRoutes.TeamMembers.Handle("", api.APISessionRequired(getTeamMembers)).Methods("GET")
    @@ -644,7 +644,7 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	userIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getTeamMembersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1376,7 +1376,7 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
     	}
     
     	memberInvite := &model.MemberInvite{}
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, memberInvite)
    +	err := model.StructFromJSONLimited(r.Body, memberInvite)
     	if err != nil {
     		c.Err = model.NewAppError("Api4.inviteUsersToTeams", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/team_local.go+1 1 modified
    @@ -79,7 +79,7 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
     	}
     
     	memberInvite := &model.MemberInvite{}
    -	err := model.StructFromJSONLimited(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes, memberInvite)
    +	err := model.StructFromJSONLimited(r.Body, memberInvite)
     	if err != nil {
     		c.Err = model.NewAppError("Api4.localInviteUsersToTeam", "api.team.invite_members_to_team_and_channels.invalid_body.app_error", nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/upload.go+2 2 modified
    @@ -17,9 +17,9 @@ import (
     )
     
     func (api *API) InitUpload() {
    -	api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload)).Methods("POST")
    +	api.BaseRoutes.Uploads.Handle("", api.APISessionRequired(createUpload, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(getUpload)).Methods("GET")
    -	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData)).Methods("POST")
    +	api.BaseRoutes.Upload.Handle("", api.APISessionRequired(uploadData, handlerParamFileAPI)).Methods("POST")
     }
     
     func createUpload(c *Context, w http.ResponseWriter, r *http.Request) {
    
  • server/channels/api4/upload_local.go+2 2 modified
    @@ -4,7 +4,7 @@
     package api4
     
     func (api *API) InitUploadLocal() {
    -	api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload)).Methods("POST")
    +	api.BaseRoutes.Uploads.Handle("", api.APILocal(createUpload, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.Upload.Handle("", api.APILocal(getUpload)).Methods("GET")
    -	api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData)).Methods("POST")
    +	api.BaseRoutes.Upload.Handle("", api.APILocal(uploadData, handlerParamFileAPI)).Methods("POST")
     }
    
  • server/channels/api4/user.go+4 4 modified
    @@ -38,7 +38,7 @@ func (api *API) InitUser() {
     	api.BaseRoutes.User.Handle("", api.APISessionRequired(getUser)).Methods("GET")
     	api.BaseRoutes.User.Handle("/image/default", api.APISessionRequiredTrustRequester(getDefaultProfileImage)).Methods("GET")
     	api.BaseRoutes.User.Handle("/image", api.APISessionRequiredTrustRequester(getProfileImage)).Methods("GET")
    -	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage)).Methods("POST")
    +	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setProfileImage, handlerParamFileAPI)).Methods("POST")
     	api.BaseRoutes.User.Handle("/image", api.APISessionRequired(setDefaultProfileImage)).Methods("DELETE")
     	api.BaseRoutes.User.Handle("", api.APISessionRequired(updateUser)).Methods("PUT")
     	api.BaseRoutes.User.Handle("/patch", api.APISessionRequired(patchUser)).Methods("PUT")
    @@ -623,7 +623,7 @@ func getFilteredUsersStats(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUsersByGroupChannelIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	channelIds, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	channelIds, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil || len(channelIds) == 0 {
     		c.Err = model.NewAppError("getUsersByGroupChannelIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -955,7 +955,7 @@ func requireGroupAccess(c *web.Context, groupID string) *model.AppError {
     }
     
     func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getUsersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    @@ -1002,7 +1002,7 @@ func getUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func getUsersByNames(c *Context, w http.ResponseWriter, r *http.Request) {
    -	usernames, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	usernames, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("getUsersByNames", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/api4/user_local.go+1 1 modified
    @@ -233,7 +233,7 @@ func localGetUsers(c *Context, w http.ResponseWriter, r *http.Request) {
     }
     
     func localGetUsersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
    -	userIDs, err := model.SortedArrayFromJSON(r.Body, *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes)
    +	userIDs, err := model.SortedArrayFromJSON(r.Body)
     	if err != nil {
     		c.Err = model.NewAppError("localGetUsersByIds", model.PayloadParseError, nil, "", http.StatusBadRequest).Wrap(err)
     		return
    
  • server/channels/web/handlers.go+26 8 modified
    @@ -31,7 +31,8 @@ import (
     )
     
     const (
    -	frameAncestors = "'self' teams.microsoft.com"
    +	frameAncestors   = "'self' teams.microsoft.com"
    +	maxURLCharacters = 2048
     )
     
     func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
    @@ -86,6 +87,7 @@ type Handler struct {
     	IsStatic                  bool
     	IsLocal                   bool
     	DisableWhenBusy           bool
    +	FileAPI                   bool
     
     	cspShaDirective string
     }
    @@ -142,7 +144,20 @@ func generateDevCSP(c Context) string {
     	return " " + strings.Join(devCSP, " ")
     }
     
    +func (h Handler) basicSecurityChecks(w http.ResponseWriter, r *http.Request) *model.AppError {
    +	if len(r.RequestURI) > maxURLCharacters {
    +		return model.NewAppError("basicSecurityChecks", "basic_security_check.url.too_long_error", nil, "", http.StatusRequestURITooLong)
    +	}
    +
    +	return nil
    +}
    +
     func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    +	if appErr := h.basicSecurityChecks(w, r); appErr != nil {
    +		http.Error(w, appErr.Error(), appErr.StatusCode)
    +		return
    +	}
    +
     	w = newWrappedWriter(w)
     	now := time.Now()
     
    @@ -213,13 +228,16 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     		c.App = app_opentracing.NewOpenTracingAppLayer(c.App, ctx)
     	}
     
    -	// Set the max request body size to be equal to MaxFileSize.
    -	// Ideally, non-file request bodies should be smaller than file request bodies,
    -	// but we don't have a clean way to identify all file upload handlers.
    -	// So to keep it simple, we clamp it to the max file size.
    -	// We add a buffer of bytes.MinRead so that file sizes close to max file size
    -	// do not get cut off.
    -	r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead)
    +	var maxBytes int64
    +	if h.FileAPI {
    +		// We add a buffer of bytes.MinRead so that file sizes close to max file size
    +		// do not get cut off.
    +		maxBytes = *c.App.Config().FileSettings.MaxFileSize + bytes.MinRead
    +	} else {
    +		maxBytes = *c.App.Config().ServiceSettings.MaximumPayloadSizeBytes + bytes.MinRead
    +	}
    +
    +	r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
     
     	subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
     	siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath
    
  • server/i18n/en.json+4 0 modified
    @@ -7254,6 +7254,10 @@
         "id": "app.webhooks.update_outgoing.app_error",
         "translation": "Unable to update the webhook."
       },
    +  {
    +    "id": "basic_security_check.url.too_long_error",
    +    "translation": "URL is too long"
    +  },
       {
         "id": "bleveengine.already_started.error",
         "translation": "Bleve is already started."
    
  • server/public/model/utils.go+6 9 modified
    @@ -501,10 +501,9 @@ func ArrayFromJSON(data io.Reader) []string {
     	return objmap
     }
     
    -func SortedArrayFromJSON(data io.Reader, maxBytes int64) ([]string, error) {
    +func SortedArrayFromJSON(data io.Reader) ([]string, error) {
     	var obj []string
    -	lr := io.LimitReader(data, maxBytes)
    -	err := json.NewDecoder(lr).Decode(&obj)
    +	err := json.NewDecoder(data).Decode(&obj)
     	if err != nil || obj == nil {
     		return nil, err
     	}
    @@ -513,10 +512,9 @@ func SortedArrayFromJSON(data io.Reader, maxBytes int64) ([]string, error) {
     	return RemoveDuplicateStrings(obj), nil
     }
     
    -func NonSortedArrayFromJSON(data io.Reader, maxBytes int64) ([]string, error) {
    +func NonSortedArrayFromJSON(data io.Reader) ([]string, error) {
     	var obj []string
    -	lr := io.LimitReader(data, maxBytes)
    -	err := json.NewDecoder(lr).Decode(&obj)
    +	err := json.NewDecoder(data).Decode(&obj)
     	if err != nil || obj == nil {
     		return nil, err
     	}
    @@ -558,9 +556,8 @@ func StringInterfaceFromJSON(data io.Reader) map[string]any {
     	return objmap
     }
     
    -func StructFromJSONLimited[V any](data io.Reader, maxBytes int64, obj *V) error {
    -	lr := io.LimitReader(data, maxBytes)
    -	err := json.NewDecoder(lr).Decode(&obj)
    +func StructFromJSONLimited[V any](data io.Reader, obj *V) error {
    +	err := json.NewDecoder(data).Decode(&obj)
     	if err != nil || obj == nil {
     		return err
     	}
    
  • server/public/model/utils_test.go+9 98 modified
    @@ -8,7 +8,6 @@ import (
     	"encoding/json"
     	"errors"
     	"fmt"
    -	"io"
     	"net/http"
     	"reflect"
     	"strings"
    @@ -242,38 +241,27 @@ func TestSortedArrayFromJSON(t *testing.T) {
     	t.Run("Successful parse", func(t *testing.T) {
     		ids := []string{NewId(), NewId(), NewId()}
     		b, _ := json.Marshal(ids)
    -		a, err := SortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.ElementsMatch(t, ids, a)
     	})
     
     	t.Run("Empty Array", func(t *testing.T) {
     		ids := []string{}
     		b, _ := json.Marshal(ids)
    -		a, err := SortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Empty(t, a)
     	})
     
    -	t.Run("Error too large", func(t *testing.T) {
    -		var ids []string
    -		for i := 0; i <= 100; i++ {
    -			ids = append(ids, NewId())
    -		}
    -		b, _ := json.Marshal(ids)
    -		_, err := SortedArrayFromJSON(bytes.NewReader(b), 1000)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("Duplicate keys, returns one", func(t *testing.T) {
     		var ids []string
     		id := NewId()
     		for i := 0; i < 10; i++ {
     			ids = append(ids, id)
     		}
     		b, _ := json.Marshal(ids)
    -		a, err := SortedArrayFromJSON(bytes.NewReader(b), 26000)
    +		a, err := SortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Len(t, a, 1)
     	})
    @@ -283,38 +271,27 @@ func TestNonSortedArrayFromJSON(t *testing.T) {
     	t.Run("Successful parse", func(t *testing.T) {
     		ids := []string{NewId(), NewId(), NewId()}
     		b, _ := json.Marshal(ids)
    -		a, err := NonSortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Equal(t, ids, a)
     	})
     
     	t.Run("Empty Array", func(t *testing.T) {
     		ids := []string{}
     		b, _ := json.Marshal(ids)
    -		a, err := NonSortedArrayFromJSON(bytes.NewReader(b), 1000)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Empty(t, a)
     	})
     
    -	t.Run("Error too large", func(t *testing.T) {
    -		var ids []string
    -		for i := 0; i <= 100; i++ {
    -			ids = append(ids, NewId())
    -		}
    -		b, _ := json.Marshal(ids)
    -		_, err := NonSortedArrayFromJSON(bytes.NewReader(b), 1000)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("Duplicate keys, returns one", func(t *testing.T) {
     		var ids []string
     		id := NewId()
     		for i := 0; i <= 10; i++ {
     			ids = append(ids, id)
     		}
     		b, _ := json.Marshal(ids)
    -		a, err := NonSortedArrayFromJSON(bytes.NewReader(b), 26000)
    +		a, err := NonSortedArrayFromJSON(bytes.NewReader(b))
     		require.NoError(t, err)
     		require.Len(t, a, 1)
     	})
    @@ -1263,7 +1240,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), b)
     		require.NoError(t, err)
     
     		require.Equal(t, b.StringField, "string")
    @@ -1272,29 +1249,6 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.Equal(t, b.BoolField, true)
     	})
     
    -	t.Run("error too big", func(t *testing.T) {
    -		type TestStruct struct {
    -			StringField string
    -			IntField    int
    -			FloatField  float32
    -			BoolField   bool
    -		}
    -
    -		testStruct := TestStruct{
    -			StringField: "string",
    -			IntField:    2,
    -			FloatField:  3.1415,
    -			BoolField:   true,
    -		}
    -		testStructBytes, err := json.Marshal(testStruct)
    -		require.NoError(t, err)
    -
    -		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 10, b)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("successfully parses nested struct", func(t *testing.T) {
     		type TestStruct struct {
     			StringField string
    @@ -1333,7 +1287,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &NestedStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), b)
     		require.NoError(t, err)
     
     		require.Equal(t, b.FieldA.StringField, "string A")
    @@ -1349,49 +1303,6 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.Equal(t, b.FieldC, []int{5, 9, 1, 5, 7})
     	})
     
    -	t.Run("errors on too big nested struct", func(t *testing.T) {
    -		type TestStruct struct {
    -			StringField string
    -			IntField    int
    -			FloatField  float32
    -			BoolField   bool
    -		}
    -
    -		type NestedStruct struct {
    -			FieldA TestStruct
    -			FieldB TestStruct
    -			FieldC []int
    -		}
    -
    -		testStructA := TestStruct{
    -			StringField: "string A",
    -			IntField:    2,
    -			FloatField:  3.1415,
    -			BoolField:   true,
    -		}
    -
    -		testStructB := TestStruct{
    -			StringField: "string B",
    -			IntField:    3,
    -			FloatField:  100,
    -			BoolField:   false,
    -		}
    -
    -		nestedStruct := NestedStruct{
    -			FieldA: testStructA,
    -			FieldB: testStructB,
    -			FieldC: []int{5, 9, 1, 5, 7},
    -		}
    -
    -		nestedStructBytes, err := json.Marshal(nestedStruct)
    -		require.NoError(t, err)
    -
    -		b := &NestedStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(nestedStructBytes), 50, b)
    -		require.Error(t, err)
    -		require.ErrorIs(t, err, io.ErrUnexpectedEOF)
    -	})
    -
     	t.Run("handles empty structs", func(t *testing.T) {
     		type TestStruct struct{}
     
    @@ -1400,7 +1311,7 @@ func TestStructFromJSONLimited(t *testing.T) {
     		require.NoError(t, err)
     
     		b := &TestStruct{}
    -		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), 1000, b)
    +		err = StructFromJSONLimited(bytes.NewReader(testStructBytes), b)
     		require.NoError(t, err)
     	})
     }
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.