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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mattermost/mattermost-serverGo | >= 8.1.0, < 8.1.12 | 8.1.12 |
github.com/mattermost/mattermost-serverGo | >= 9.5.0, < 9.5.3 | 9.5.3 |
github.com/mattermost/mattermost-serverGo | >= 9.6.0-rc1, < 9.6.1 | 9.6.1 |
Affected products
1- Range: 8.1.0
Patches
449e7c477246eCherrypicking API Handler params for 8.1 (#26564)
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) }) }
f6d320017549Cherrypicking API Handler params for 9.5 (#26563)
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) }) }
13049d8b16b1Manual cherrypick for API handler options for v9.6 (#26560)
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) {
54478f2ccbc6API handler opts modifier (#26148) (#26550)
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- github.com/advisories/GHSA-p2wq-4ggp-45f3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-22091ghsaADVISORY
- github.com/mattermost/mattermost/commit/13049d8b16b195f98246dff4812b5f64c1e5a627ghsaWEB
- github.com/mattermost/mattermost/commit/49e7c477246e31c7a0bd85c1043599121755b260ghsaWEB
- github.com/mattermost/mattermost/commit/54478f2ccbc6c4f110706966adfe0db2c16a566cghsaWEB
- github.com/mattermost/mattermost/commit/f6d320017549ec66efb5fdd4bc10b66ab36abb70ghsaWEB
- mattermost.com/security-updatesghsaWEB
News mentions
0No linked articles in our index yet.