From 1ca9b46cdf60841441eb6cd69775405f7008a483 Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Sat, 8 Aug 2026 09:50:27 +0700 Subject: [PATCH] fix(github): sanitize PR review and discussion bodies on read paths Issue/PR title and body reads already run through sanitize.Sanitize. PR review bodies, review-thread comments, and Discussion title/body/comments still returned raw attacker-controlled text into the model context. Apply the same sanitize control on those sibling read paths. Clean content is unchanged; lockdown filtering is untouched. --- pkg/github/discussions.go | 13 +++++++------ pkg/github/minimal_types.go | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pkg/github/discussions.go b/pkg/github/discussions.go index 8643acc7ef..59f2717ffe 100644 --- a/pkg/github/discussions.go +++ b/pkg/github/discussions.go @@ -8,6 +8,7 @@ import ( "github.com/github/github-mcp-server/pkg/ifc" "github.com/github/github-mcp-server/pkg/inventory" + "github.com/github/github-mcp-server/pkg/sanitize" "github.com/github/github-mcp-server/pkg/scopes" "github.com/github/github-mcp-server/pkg/translations" "github.com/github/github-mcp-server/pkg/utils" @@ -99,7 +100,7 @@ type WithCategoryNoOrder struct { func fragmentToDiscussion(fragment NodeFragment) *github.Discussion { return &github.Discussion{ Number: github.Ptr(int(fragment.Number)), - Title: github.Ptr(string(fragment.Title)), + Title: github.Ptr(sanitize.Sanitize(string(fragment.Title))), HTMLURL: github.Ptr(string(fragment.URL)), CreatedAt: &github.Timestamp{Time: fragment.CreatedAt.Time}, UpdatedAt: &github.Timestamp{Time: fragment.UpdatedAt.Time}, @@ -360,8 +361,8 @@ func GetDiscussion(t translations.TranslationHelperFunc) inventory.ServerTool { // like ListDiscussions and GetDiscussionComments). response := map[string]any{ "number": int(d.Number), - "title": string(d.Title), - "body": string(d.Body), + "title": sanitize.Sanitize(string(d.Title)), + "body": sanitize.Sanitize(string(d.Body)), "url": string(d.URL), "closed": bool(d.Closed), "isAnswered": bool(d.IsAnswered), @@ -522,14 +523,14 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve for _, c := range q.Repository.Discussion.Comments.Nodes { comment := MinimalDiscussionComment{ ID: fmt.Sprintf("%v", c.ID), - Body: string(c.Body), + Body: sanitize.Sanitize(string(c.Body)), IsAnswer: bool(c.IsAnswer), ReplyTotalCount: c.Replies.TotalCount, } for _, r := range c.Replies.Nodes { comment.Replies = append(comment.Replies, MinimalDiscussionComment{ ID: fmt.Sprintf("%v", r.ID), - Body: string(r.Body), + Body: sanitize.Sanitize(string(r.Body)), IsAnswer: bool(r.IsAnswer), }) } @@ -564,7 +565,7 @@ func GetDiscussionComments(t translations.TranslationHelperFunc) inventory.Serve for _, c := range q.Repository.Discussion.Comments.Nodes { comments = append(comments, MinimalDiscussionComment{ ID: fmt.Sprintf("%v", c.ID), - Body: string(c.Body), + Body: sanitize.Sanitize(string(c.Body)), IsAnswer: bool(c.IsAnswer), }) } diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index e2bf8b684b..8cc20701c8 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -645,7 +645,7 @@ func convertToMinimalPullRequestReview(review *github.PullRequestReview) Minimal m := MinimalPullRequestReview{ ID: review.GetID(), State: review.GetState(), - Body: review.GetBody(), + Body: sanitize.Sanitize(review.GetBody()), HTMLURL: review.GetHTMLURL(), User: convertToMinimalUser(review.GetUser()), CommitID: review.GetCommitID(), @@ -1909,7 +1909,7 @@ func convertToMinimalReviewThread(thread reviewThreadNode) MinimalReviewThread { func convertToMinimalReviewComment(c reviewCommentNode) MinimalReviewComment { m := MinimalReviewComment{ - Body: string(c.Body), + Body: sanitize.Sanitize(string(c.Body)), Path: string(c.Path), Author: string(c.Author.Login), HTMLURL: c.URL.String(),