VYPR
High severityOSV Advisory· Published Dec 22, 2025· Updated Dec 22, 2025

Mattermost Jira plugin user spoofing enables Jira request forgery.

CVE-2025-14273

Description

Mattermost versions 11.1.x <= 11.1.0, 11.0.x <= 11.0.5, 10.12.x <= 10.12.3, 10.11.x <= 10.11.7 with the Jira plugin enabled and Mattermost Jira plugin versions <=4.4.0 fail to enforce authentication and issue-key path restrictions in the Jira plugin, which allows an unauthenticated attacker who knows a valid user ID to issue authenticated GET and POST requests to the Jira server via crafted plugin payloads that spoof the user ID and inject arbitrary issue key paths. Mattermost Advisory ID: MMSA-2025-00555

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20251121122154-b57c297c6d7a8.0.0-20251121122154-b57c297c6d7a
github.com/mattermost/mattermost-plugin-jiraGo
< 4.4.14.4.1

Affected products

1

Patches

6
317025c411ec

Update Jira prepackaged (#34551) (#34569)

https://github.com/mattermost/mattermostJust NevNov 21, 2025via ghsa
1 file changed · +1 1
  • server/Makefile+1 1 modified
    @@ -155,7 +155,7 @@ PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
     PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
     PLUGIN_PACKAGES += mattermost-plugin-github-v2.5.0
     PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.11.0
    -PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.0
    +PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.1
     PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.5.1
     PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.3.4
     PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.8.0
    
92b1e705225d

Update Jira prepackaged (#34551) (#34568)

https://github.com/mattermost/mattermostJust NevNov 21, 2025via ghsa
1 file changed · +1 1
  • server/Makefile+1 1 modified
    @@ -155,7 +155,7 @@ PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
     PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
     PLUGIN_PACKAGES += mattermost-plugin-github-v2.4.0
     PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.10.0
    -PLUGIN_PACKAGES += mattermost-plugin-jira-v4.3.0
    +PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.1
     PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.4.2
     PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.3.4
     PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.8.0
    
7c36acb68ce3

Update Jira prepackaged (#34551) (#34570)

https://github.com/mattermost/mattermostJust NevNov 21, 2025via ghsa
1 file changed · +1 1
  • server/Makefile+1 1 modified
    @@ -145,7 +145,7 @@ PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
     PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
     PLUGIN_PACKAGES += mattermost-plugin-github-v2.5.0
     PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.10.0
    -PLUGIN_PACKAGES += mattermost-plugin-jira-v4.3.0
    +PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.1
     # We need to prepackage both versions of playbooks and install the correct one based on the server license. See MM-60025.
     PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.41.1
     PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.3.0
    
463e0d0d3930

Update Jira prepackaged (#34551) (#34567)

https://github.com/mattermost/mattermostJust NevNov 21, 2025via ghsa
1 file changed · +1 1
  • server/Makefile+1 1 modified
    @@ -145,7 +145,7 @@ PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
     PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
     PLUGIN_PACKAGES += mattermost-plugin-github-v2.4.0
     PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.10.0
    -PLUGIN_PACKAGES += mattermost-plugin-jira-v4.3.0
    +PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.1
     # We need to prepackage both versions of playbooks and install the correct one based on the server license. See MM-60025.
     PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.41.1
     PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.3.0
    
b57c297c6d7a

Update Jira prepackaged (#34551)

https://github.com/mattermost/mattermostMaria A NunezNov 21, 2025via ghsa
1 file changed · +1 1
  • server/Makefile+1 1 modified
    @@ -155,7 +155,7 @@ PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
     PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
     PLUGIN_PACKAGES += mattermost-plugin-github-v2.5.0
     PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.11.0
    -PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.0
    +PLUGIN_PACKAGES += mattermost-plugin-jira-v4.4.1
     PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.6.0
     PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.4.0
     PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.10.0
    
bf9a1b7e81eb

feat: Secure Jira post actions against forged requests (#1243)

2 files changed · +350 51
  • server/issue.go+76 7 modified
    @@ -22,6 +22,8 @@ import (
     	"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
     )
     
    +var issueKeyRegexp = regexp.MustCompile(`^[A-Z][A-Z0-9_]+-\d+$`)
    +
     const (
     	assigneeField            = "assignee"
     	labelsField              = "labels"
    @@ -62,6 +64,11 @@ func makePost(userID, channelID, message string) *model.Post {
     }
     
     func (p *Plugin) httpShareIssuePublicly(w http.ResponseWriter, r *http.Request) (int, error) {
    +	authenticatedUserID := strings.TrimSpace(r.Header.Get(headerMattermostUserID))
    +	if authenticatedUserID == "" {
    +		return respondErr(w, http.StatusUnauthorized, errors.New("missing Mattermost-User-ID header"))
    +	}
    +
     	var requestData model.PostActionIntegrationRequest
     	err := json.NewDecoder(r.Body).Decode(&requestData)
     	if err != nil {
    @@ -71,18 +78,47 @@ func (p *Plugin) httpShareIssuePublicly(w http.ResponseWriter, r *http.Request)
     
     	jiraBotID := p.getUserID()
     	channelID := requestData.ChannelId
    -	mattermostUserID := requestData.UserId
    -	if mattermostUserID == "" {
    -		return p.respondErrWithFeedback(mattermostUserID, makePost(jiraBotID, channelID,
    +
    +	postID := strings.TrimSpace(requestData.PostId)
    +	if postID == "" {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
    +			"missing post id"), w, http.StatusBadRequest)
    +	}
    +
    +	originalPost, appErr := p.client.Post.GetPost(postID)
    +	if appErr != nil || originalPost == nil {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
    +			"post not found"), w, http.StatusNotFound)
    +	}
    +	if originalPost.UserId != jiraBotID {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
     			"user not authorized"), w, http.StatusUnauthorized)
     	}
     
    +	if originalPost.ChannelId != channelID {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
    +			"channel mismatch"), w, http.StatusBadRequest)
    +	}
    +
    +	if requestData.UserId != "" && requestData.UserId != authenticatedUserID {
    +		p.client.Log.Warn("share issue payload user mismatch",
    +			"header_user_id", authenticatedUserID,
    +			"payload_user_id", requestData.UserId)
    +	}
    +
    +	mattermostUserID := authenticatedUserID
    +
     	val := requestData.Context["issue_key"]
     	issueKey, ok := val.(string)
     	if !ok {
     		return p.respondErrWithFeedback(mattermostUserID, makePost(jiraBotID, channelID,
     			"No issue key was found in context data"), w, http.StatusInternalServerError)
     	}
    +	issueKey = strings.ToUpper(strings.TrimSpace(issueKey))
    +	if !issueKeyRegexp.MatchString(issueKey) {
    +		return p.respondErrWithFeedback(mattermostUserID, makePost(jiraBotID, channelID,
    +			"invalid issue key"), w, http.StatusBadRequest)
    +	}
     
     	val = requestData.Context["instance_id"]
     	instanceID, ok := val.(string)
    @@ -97,7 +133,7 @@ func (p *Plugin) httpShareIssuePublicly(w http.ResponseWriter, r *http.Request)
     			"No connection could be loaded with given params"), w, http.StatusInternalServerError)
     	}
     
    -	attachment, err := p.getIssueAsSlackAttachment(instance, connection, strings.ToUpper(issueKey), false)
    +	attachment, err := p.getIssueAsSlackAttachment(instance, connection, issueKey, false)
     	if err != nil {
     		return p.respondErrWithFeedback(mattermostUserID, makePost(jiraBotID, channelID,
     			"Could not get issue as slack attachment"), w, http.StatusInternalServerError)
    @@ -123,6 +159,11 @@ func (p *Plugin) httpShareIssuePublicly(w http.ResponseWriter, r *http.Request)
     }
     
     func (p *Plugin) httpTransitionIssuePostAction(w http.ResponseWriter, r *http.Request) (int, error) {
    +	authenticatedUserID := strings.TrimSpace(r.Header.Get(headerMattermostUserID))
    +	if authenticatedUserID == "" {
    +		return respondErr(w, http.StatusUnauthorized, errors.New("missing Mattermost-User-ID header"))
    +	}
    +
     	var requestData model.PostActionIntegrationRequest
     	err := json.NewDecoder(r.Body).Decode(&requestData)
     	if err != nil {
    @@ -133,18 +174,46 @@ func (p *Plugin) httpTransitionIssuePostAction(w http.ResponseWriter, r *http.Re
     	jiraBotID := p.getUserID()
     	channelID := requestData.ChannelId
     
    -	mattermostUserID := requestData.UserId
    -	if mattermostUserID == "" {
    -		return p.respondErrWithFeedback(mattermostUserID, makePost(jiraBotID, channelID,
    +	postID := strings.TrimSpace(requestData.PostId)
    +	if postID == "" {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
    +			"missing post id"), w, http.StatusBadRequest)
    +	}
    +
    +	originalPost, appErr := p.client.Post.GetPost(postID)
    +	if appErr != nil || originalPost == nil {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
    +			"post not found"), w, http.StatusNotFound)
    +	}
    +	if originalPost.UserId != jiraBotID {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
     			"user not authorized"), w, http.StatusUnauthorized)
     	}
     
    +	if originalPost.ChannelId != channelID {
    +		return p.respondErrWithFeedback(authenticatedUserID, makePost(jiraBotID, channelID,
    +			"channel mismatch"), w, http.StatusBadRequest)
    +	}
    +
    +	if requestData.UserId != "" && requestData.UserId != authenticatedUserID {
    +		p.client.Log.Warn("transition payload user mismatch",
    +			"header_user_id", authenticatedUserID,
    +			"payload_user_id", requestData.UserId)
    +	}
    +
    +	mattermostUserID := authenticatedUserID
    +
     	val := requestData.Context["issue_key"]
     	issueKey, ok := val.(string)
     	if !ok {
     		return p.respondErrWithFeedback(mattermostUserID, makePost(jiraBotID, channelID,
     			"No issue key was found in context data"), w, http.StatusInternalServerError)
     	}
    +	issueKey = strings.ToUpper(strings.TrimSpace(issueKey))
    +	if !issueKeyRegexp.MatchString(issueKey) {
    +		return p.respondErrWithFeedback(mattermostUserID, makePost(jiraBotID, channelID,
    +			"invalid issue key"), w, http.StatusBadRequest)
    +	}
     
     	val = requestData.Context["selected_option"]
     	toState, ok := val.(string)
    
  • server/issue_test.go+274 44 modified
    @@ -185,125 +185,355 @@ func TestTransitionJiraIssue(t *testing.T) {
     }
     
     func TestRouteIssueTransition(t *testing.T) {
    +	const (
    +		headerUserID       = "user-id"
    +		botUserID          = "jira-bot"
    +		validPostID        = "valid-post"
    +		wrongAuthorPostID  = "wrong-author-post"
    +		missingPostID      = "missing-post"
    +		wrongChannelPostID = "wrong-channel-post"
    +	)
    +
     	api := &plugintest.API{}
     	api.On("SendEphemeralPost", mock.AnythingOfType("string"), mock.AnythingOfType("*model.Post")).Return(&model.Post{})
    -	api.On("LogWarn", "ERROR: ", "Status", "401", "Error", "", "Path", "/api/v2/transition", "Method", "POST", "query", "").Return(nil)
    -	api.On("LogWarn", "ERROR: ", "Status", "500", "Error", "", "Path", "/api/v2/transition", "Method", "POST", "query", "").Return(nil)
    -	api.On("LogWarn", "Recovered from a panic", "url", "/api/v2/transition", "error", mock.Anything, "stack", mock.Anything).Return(nil)
    +	api.On("LogWarn", "ERROR: ", "Status", "401", "Error", "", "Path", "/api/v2/transition", "Method", "POST", "query", "").Return(nil).Maybe()
    +	api.On("LogWarn", "ERROR: ", "Status", "400", "Error", "", "Path", "/api/v2/transition", "Method", "POST", "query", "").Return(nil).Maybe()
    +	api.On("LogWarn", "ERROR: ", "Status", "404", "Error", "", "Path", "/api/v2/transition", "Method", "POST", "query", "").Return(nil).Maybe()
    +	api.On("LogWarn", "ERROR: ", "Status", "500", "Error", "", "Path", "/api/v2/transition", "Method", "POST", "query", "").Return(nil).Maybe()
    +	api.On("LogWarn", "Recovered from a panic", "url", "/api/v2/transition", "error", mock.Anything, "stack", mock.Anything).Return(nil).Maybe()
    +	api.On("LogWarn", "transition payload user mismatch", "header_user_id", headerUserID, "payload_user_id", "different-user").Return().Maybe()
    +	api.On("GetPost", validPostID).Return(&model.Post{Id: validPostID, UserId: botUserID, ChannelId: "channel-id"}, nil)
    +	api.On("GetPost", wrongAuthorPostID).Return(&model.Post{Id: wrongAuthorPostID, UserId: "not-bot"}, nil)
    +	api.On("GetPost", missingPostID).Return(nil, (*model.AppError)(nil))
    +	api.On("GetPost", wrongChannelPostID).Return(&model.Post{Id: wrongChannelPostID, UserId: botUserID, ChannelId: "different-channel"}, nil)
    +	api.On("DeleteEphemeralPost", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil).Maybe()
     
     	p := setupTestPlugin(api)
    +	p.updateConfig(func(conf *config) {
    +		conf.botUserID = botUserID
    +	})
     
    -	for name, tt := range map[string]struct {
    -		bb           []byte
    +	cases := map[string]struct {
    +		header       string
     		request      *model.PostActionIntegrationRequest
     		expectedCode int
     	}{
    -		"No request data": {
    +		"Missing header": {
    +			header:       "",
     			request:      nil,
     			expectedCode: http.StatusUnauthorized,
     		},
    -		"No UserID": {
    +		"Missing post id": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    "",
    +			},
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		"Post not found": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId: "",
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    missingPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":       "TEST-10",
    +					"selected_option": "31",
    +					"instance_id":     testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusNotFound,
    +		},
    +		"Post not from Jira bot": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    wrongAuthorPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":       "TEST-10",
    +					"selected_option": "31",
    +					"instance_id":     testInstance1.InstanceID.String(),
    +				},
     			},
     			expectedCode: http.StatusUnauthorized,
     		},
    -		"No issueKey": {
    +		"Channel mismatch": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId: "userID",
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    wrongChannelPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":       "TEST-10",
    +					"selected_option": "31",
    +					"instance_id":     testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		"Invalid issue key": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":       "bad",
    +					"selected_option": "31",
    +					"instance_id":     testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		"Missing transition option": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":   "TEST-10",
    +					"instance_id": testInstance1.InstanceID.String(),
    +				},
     			},
     			expectedCode: http.StatusInternalServerError,
     		},
    -		"No selected_option": {
    +		"Missing instance id": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId:  "userID",
    -				Context: map[string]interface{}{"issueKey": "Some-Key"},
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":       "TEST-10",
    +					"selected_option": "31",
    +				},
     			},
     			expectedCode: http.StatusInternalServerError,
     		},
    -	} {
    +		"Payload mismatch allowed": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    "different-user",
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":       "TEST-10",
    +					"selected_option": "31",
    +					"instance_id":     testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusInternalServerError,
    +		},
    +		"Happy Path": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":       "TEST-10",
    +					"selected_option": "31",
    +					"instance_id":     testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusInternalServerError,
    +		},
    +	}
    +
    +	for name, tt := range cases {
     		t.Run(name, func(t *testing.T) {
    -			bb, err := json.Marshal(tt.request)
    -			assert.Nil(t, err)
    +			body := ""
    +			if tt.request != nil {
    +				bb, err := json.Marshal(tt.request)
    +				assert.Nil(t, err)
    +				body = string(bb)
    +			}
    +
    +			req := httptest.NewRequest("POST", makeAPIRoute(routeIssueTransition), strings.NewReader(body))
    +			if tt.header != "" {
    +				req.Header.Set(headerMattermostUserID, tt.header)
    +			}
     
    -			request := httptest.NewRequest("POST", makeAPIRoute(routeIssueTransition), strings.NewReader(string(bb)))
     			w := httptest.NewRecorder()
    -			p.ServeHTTP(&plugin.Context{}, w, request)
    -			assert.Equal(t, tt.expectedCode, w.Result().StatusCode, "no request data")
    +			p.ServeHTTP(&plugin.Context{}, w, req)
    +			assert.Equal(t, tt.expectedCode, w.Result().StatusCode, "status code mismatch")
     		})
     	}
     }
     
     func TestRouteShareIssuePublicly(t *testing.T) {
    -	validUserID := "1"
    +	const (
    +		headerUserID       = "1"
    +		botUserID          = "jira-bot"
    +		validPostID        = "valid-post"
    +		wrongAuthorPostID  = "wrong-author-post"
    +		missingPostID      = "missing-post"
    +		wrongChannelPostID = "wrong-channel-post"
    +	)
    +
     	api := &plugintest.API{}
     	api.On("SendEphemeralPost", mock.AnythingOfType("string"), mock.AnythingOfType("*model.Post")).Return(&model.Post{})
     	api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(&model.Post{}, nil)
    -	api.On("DeleteEphemeralPost", validUserID, "").Return()
    -	api.On("LogWarn", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return()
    -	api.On("LogWarn", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return()
    +	api.On("DeleteEphemeralPost", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil).Maybe()
    +	api.On("GetPost", validPostID).Return(&model.Post{Id: validPostID, UserId: botUserID, ChannelId: "channel-id"}, nil)
    +	api.On("GetPost", wrongAuthorPostID).Return(&model.Post{Id: wrongAuthorPostID, UserId: "someone-else"}, nil)
    +	api.On("GetPost", missingPostID).Return(nil, (*model.AppError)(nil))
    +	api.On("GetPost", wrongChannelPostID).Return(&model.Post{Id: wrongChannelPostID, UserId: botUserID, ChannelId: "different-channel"}, nil)
    +	api.On("LogWarn", "share issue payload user mismatch", "header_user_id", headerUserID, "payload_user_id", "someone-else").Return().Maybe()
    +	api.On("LogWarn", "ERROR: ", "Status", "400", "Error", "", "Path", "/api/v2/share-issue-publicly", "Method", "POST", "query", "").Return(nil).Maybe()
    +	api.On("LogWarn", "ERROR: ", "Status", "404", "Error", "", "Path", "/api/v2/share-issue-publicly", "Method", "POST", "query", "").Return(nil).Maybe()
    +	api.On("LogWarn", "ERROR: ", "Status", "500", "Error", "", "Path", "/api/v2/share-issue-publicly", "Method", "POST", "query", "").Return(nil).Maybe()
    +	api.On("LogWarn", "Recovered from a panic", "url", "/api/v2/share-issue-publicly", "error", mock.Anything, "stack", mock.Anything).Return(nil).Maybe()
     
     	p := setupTestPlugin(api)
    +	p.updateConfig(func(conf *config) {
    +		conf.botUserID = botUserID
    +	})
     
    -	for name, tt := range map[string]struct {
    -		bb           []byte
    +	cases := map[string]struct {
    +		header       string
     		request      *model.PostActionIntegrationRequest
     		expectedCode int
     	}{
    -		"No request data": {
    +		"Missing header": {
    +			header:       "",
     			request:      nil,
     			expectedCode: http.StatusUnauthorized,
     		},
    -		"No UserID": {
    +		"Missing post id": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId: "",
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    "",
    +			},
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		"Post not found": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    missingPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":   "TEST-10",
    +					"instance_id": testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusNotFound,
    +		},
    +		"Post not from Jira bot": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    wrongAuthorPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":   "TEST-10",
    +					"instance_id": testInstance1.InstanceID.String(),
    +				},
     			},
     			expectedCode: http.StatusUnauthorized,
     		},
    -		"No issueKey": {
    +		"Channel mismatch": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId: "userID",
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    wrongChannelPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":   "TEST-10",
    +					"instance_id": testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		"Missing issue key": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
    +				Context: map[string]interface{}{
    +					"instance_id": testInstance1.InstanceID.String(),
    +				},
     			},
     			expectedCode: http.StatusInternalServerError,
     		},
    -		"No instanceId": {
    +		"Invalid issue key": {
    +			header: headerUserID,
    +			request: &model.PostActionIntegrationRequest{
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
    +				Context: map[string]interface{}{
    +					"issue_key":   "bad",
    +					"instance_id": testInstance1.InstanceID.String(),
    +				},
    +			},
    +			expectedCode: http.StatusBadRequest,
    +		},
    +		"Missing instance id": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId: "userID",
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
     				Context: map[string]interface{}{
     					"issue_key": "TEST-10",
     				},
     			},
     			expectedCode: http.StatusInternalServerError,
     		},
    -		"No connection": {
    +		"Happy Path": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId: "userID",
    +				UserId:    headerUserID,
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
     				Context: map[string]interface{}{
     					"issue_key":   "TEST-10",
    -					"instance_id": "id",
    +					"instance_id": testInstance1.InstanceID.String(),
     				},
     			},
    -			expectedCode: http.StatusInternalServerError,
    +			expectedCode: http.StatusOK,
     		},
    -		"Happy Path": {
    +		"Payload mismatch allowed": {
    +			header: headerUserID,
     			request: &model.PostActionIntegrationRequest{
    -				UserId: validUserID,
    +				UserId:    "someone-else",
    +				ChannelId: "channel-id",
    +				PostId:    validPostID,
     				Context: map[string]interface{}{
     					"issue_key":   "TEST-10",
     					"instance_id": testInstance1.InstanceID.String(),
     				},
     			},
     			expectedCode: http.StatusOK,
     		},
    -	} {
    +	}
    +
    +	for name, tt := range cases {
     		t.Run(name, func(t *testing.T) {
    -			bb, err := json.Marshal(tt.request)
    -			assert.Nil(t, err)
    +			body := ""
    +			if tt.request != nil {
    +				bb, err := json.Marshal(tt.request)
    +				assert.Nil(t, err)
    +				body = string(bb)
    +			}
    +
    +			req := httptest.NewRequest("POST", makeAPIRoute(routeSharePublicly), strings.NewReader(body))
    +			if tt.header != "" {
    +				req.Header.Set(headerMattermostUserID, tt.header)
    +			}
     
    -			request := httptest.NewRequest("POST", makeAPIRoute(routeSharePublicly), strings.NewReader(string(bb)))
     			w := httptest.NewRecorder()
    -			p.ServeHTTP(&plugin.Context{}, w, request)
    -			assert.Equal(t, tt.expectedCode, w.Result().StatusCode, "no request data")
    +			p.ServeHTTP(&plugin.Context{}, w, req)
    +			assert.Equal(t, tt.expectedCode, w.Result().StatusCode, "status code mismatch")
     		})
     	}
     }
    

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

9

News mentions

0

No linked articles in our index yet.