[MM-67074] Integration Action memory use fix (#34896)

This commit is contained in:
Christopher Poile 2026-01-27 11:54:11 -05:00 committed by GitHub
parent fbe5ad0ea2
commit fe3052073d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 115 additions and 2 deletions

View file

@ -252,7 +252,8 @@ func (a *App) DoPostActionWithCookie(rctx request.CTX, postID, actionId, userID,
defer resp.Body.Close()
var response model.PostActionIntegrationResponse
respBytes, err := io.ReadAll(resp.Body)
limitedReader := io.LimitReader(resp.Body, MaxIntegrationResponseSize)
respBytes, err := io.ReadAll(limitedReader)
if err != nil {
return "", model.NewAppError("DoPostActionWithCookie", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}

View file

@ -173,6 +173,118 @@ func TestPostActionEmptyResponse(t *testing.T) {
})
}
// infiniteReader generates unlimited data for testing response size limits
type infiniteReader struct{}
func (r infiniteReader) Read(p []byte) (n int, err error) {
for i := range p {
p[i] = 'a'
}
return len(p), nil
}
// MM-67074: TestPostActionResponseSizeLimit verifies that DoPostActionWithCookie
// properly limits response sizes to prevent OOM attacks
func TestPostActionResponseSizeLimit(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
channel := th.BasicChannel
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
})
t.Run("large valid JSON response is truncated", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Send response larger than MaxIntegrationResponseSize (1MB)
// Response starts as valid JSON but becomes truncated
_, _ = io.Copy(w, io.MultiReader(
strings.NewReader(`{"update":{"message":"`),
infiniteReader{},
strings.NewReader(`"}}`),
))
}))
defer server.Close()
interactivePost := model.Post{
Message: "Interactive post",
ChannelId: channel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
model.PostPropsAttachments: []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Type: model.PostActionTypeButton,
Name: "action",
Integration: &model.PostActionIntegration{
URL: server.URL,
},
},
},
},
},
},
}
post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true)
require.Nil(t, err)
attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment)
require.True(t, ok)
// Should return error due to truncated JSON, but NOT crash or OOM
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id,
attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
require.NotNil(t, err)
// Truncated JSON causes unmarshal error
assert.Equal(t, "api.post.do_action.action_integration.app_error", err.Id)
})
t.Run("large invalid response is truncated", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Send infinite non-JSON data
_, _ = io.Copy(w, infiniteReader{})
}))
defer server.Close()
interactivePost := model.Post{
Message: "Interactive post",
ChannelId: channel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
Props: model.StringInterface{
model.PostPropsAttachments: []*model.SlackAttachment{
{
Text: "hello",
Actions: []*model.PostAction{
{
Type: model.PostActionTypeButton,
Name: "action",
Integration: &model.PostActionIntegration{
URL: server.URL,
},
},
},
},
},
},
}
post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true)
require.Nil(t, err)
attachments, ok := post.GetProp(model.PostPropsAttachments).([]*model.SlackAttachment)
require.True(t, ok)
// Should return error due to invalid JSON, but NOT crash or OOM
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id,
attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
require.NotNil(t, err)
assert.Equal(t, "api.post.do_action.action_integration.app_error", err.Id)
})
}
func TestPostAction(t *testing.T) {
mainHelper.Parallel(t)
testCases := []struct {
@ -1236,7 +1348,7 @@ func TestLookupInteractiveDialog(t *testing.T) {
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
var request model.SubmitDialogRequest
json.NewDecoder(r.Body).Decode(&request)
response := &model.LookupDialogResponse{
Items: []model.DialogSelectOption{
{Text: "Plugin Option 1", Value: "plugin_value1"},