Skip to content

Commit c2840e9

Browse files
nevyangelovaNevyana Angelova
andauthored
Fix input validation in PR details and issue creation endpoints (#988)
* Fix input validation in PR details and issue creation endpoints * Coderabbit review * PR feedback * Harden input validation for edge cases Made-with: Cursor --------- Co-authored-by: Nevyana Angelova <nevyangelova@192.168.100.29>
1 parent 2feed06 commit c2840e9

File tree

2 files changed

+152
-10
lines changed

2 files changed

+152
-10
lines changed

server/plugin/api.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -765,9 +765,21 @@ func (p *Plugin) getPrsDetails(c *UserContext, w http.ResponseWriter, r *http.Re
765765
return
766766
}
767767

768-
prDetails := make([]*PRDetails, len(prList))
768+
var validPRs []*PRDetails
769+
for _, pr := range prList {
770+
if pr == nil {
771+
continue
772+
}
773+
if _, _, err := getRepoOwnerAndNameFromURL(pr.URL); err != nil {
774+
c.Log.WithError(err).Warnf("Skipping PR with invalid URL")
775+
continue
776+
}
777+
validPRs = append(validPRs, pr)
778+
}
779+
780+
prDetails := make([]*PRDetails, len(validPRs))
769781
var wg sync.WaitGroup
770-
for i, pr := range prList {
782+
for i, pr := range validPRs {
771783
wg.Go(func() {
772784
prDetail := p.fetchPRDetails(c, githubClient, pr.URL, pr.Number)
773785
prDetails[i] = prDetail
@@ -786,7 +798,16 @@ func (p *Plugin) fetchPRDetails(c *UserContext, client *github.Client, prURL str
786798
requestedReviewers := []*string{}
787799
reviewsList := []*github.PullRequestReview{}
788800

789-
repoOwner, repoName := getRepoOwnerAndNameFromURL(prURL)
801+
repoOwner, repoName, err := getRepoOwnerAndNameFromURL(prURL)
802+
if err != nil {
803+
c.Log.WithError(err).Warnf("Invalid PR URL")
804+
return &PRDetails{
805+
URL: prURL,
806+
Number: prNumber,
807+
RequestedReviewers: requestedReviewers,
808+
Reviews: reviewsList,
809+
}
810+
}
790811

791812
var wg sync.WaitGroup
792813

@@ -841,9 +862,34 @@ func fetchReviews(c *UserContext, client *github.Client, repoOwner string, repoN
841862
return reviewsList, nil
842863
}
843864

844-
func getRepoOwnerAndNameFromURL(url string) (string, string) {
845-
splitted := strings.Split(url, "/")
846-
return splitted[len(splitted)-2], splitted[len(splitted)-1]
865+
func getRepoOwnerAndNameFromURL(rawURL string) (string, string, error) {
866+
if rawURL == "" {
867+
return "", "", fmt.Errorf("URL must not be empty")
868+
}
869+
870+
var segments []string
871+
if parsed, err := url.Parse(rawURL); err == nil && parsed.Scheme != "" {
872+
segments = strings.Split(strings.Trim(parsed.Path, "/"), "/")
873+
} else {
874+
segments = strings.Split(strings.Trim(rawURL, "/"), "/")
875+
}
876+
877+
hasReposSegment := false
878+
for i, seg := range segments {
879+
if seg == "repos" {
880+
hasReposSegment = true
881+
if i+2 < len(segments) && segments[i+1] != "" && segments[i+2] != "" {
882+
return segments[i+1], segments[i+2], nil
883+
}
884+
break
885+
}
886+
}
887+
888+
if !hasReposSegment && len(segments) == 2 && segments[0] != "" && segments[1] != "" {
889+
return segments[0], segments[1], nil
890+
}
891+
892+
return "", "", fmt.Errorf("invalid repository URL %q: expected owner/repo or a GitHub API URL containing /repos/owner/repo", rawURL)
847893
}
848894

849895
func (p *Plugin) searchIssues(c *UserContext, w http.ResponseWriter, r *http.Request) {
@@ -1726,9 +1772,11 @@ func (p *Plugin) createIssue(c *UserContext, w http.ResponseWriter, r *http.Requ
17261772
return
17271773
}
17281774

1729-
splittedRepo := strings.Split(issue.Repo, "/")
1730-
owner := splittedRepo[0]
1731-
repoName := splittedRepo[1]
1775+
owner, repoName, err := parseRepo(issue.Repo)
1776+
if err != nil {
1777+
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "invalid repository: " + issue.Repo, StatusCode: http.StatusBadRequest})
1778+
return
1779+
}
17321780

17331781
githubClient := p.githubConnectUser(c.Ctx, c.GHInfo)
17341782
var resp *github.Response
@@ -1821,7 +1869,7 @@ func parseRepo(repoParam string) (owner, repo string, err error) {
18211869
}
18221870

18231871
splitted := strings.Split(repoParam, "/")
1824-
if len(splitted) != 2 {
1872+
if len(splitted) != 2 || splitted[0] == "" || splitted[1] == "" {
18251873
return "", "", errors.New("invalid repository")
18261874
}
18271875

server/plugin/api_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,26 @@ func TestParseRepo(t *testing.T) {
502502
assert.EqualError(t, err, "invalid repository")
503503
},
504504
},
505+
{
506+
name: "Empty owner",
507+
repoParam: "/repo",
508+
setup: func() {},
509+
assertions: func(t *testing.T, owner, repo string, err error) {
510+
assert.Equal(t, "", owner)
511+
assert.Equal(t, "", repo)
512+
assert.EqualError(t, err, "invalid repository")
513+
},
514+
},
515+
{
516+
name: "Empty repo",
517+
repoParam: "owner/",
518+
setup: func() {},
519+
assertions: func(t *testing.T, owner, repo string, err error) {
520+
assert.Equal(t, "", owner)
521+
assert.Equal(t, "", repo)
522+
assert.EqualError(t, err, "invalid repository")
523+
},
524+
},
505525
}
506526
for _, tc := range tests {
507527
t.Run(tc.name, func(t *testing.T) {
@@ -514,6 +534,80 @@ func TestParseRepo(t *testing.T) {
514534
}
515535
}
516536

537+
func TestGetRepoOwnerAndNameFromURL(t *testing.T) {
538+
tests := []struct {
539+
name string
540+
url string
541+
expectedOwner string
542+
expectedRepo string
543+
expectError bool
544+
}{
545+
{
546+
name: "GitHub API repository URL",
547+
url: "https://api.github.com/repos/owner/repo",
548+
expectedOwner: "owner",
549+
expectedRepo: "repo",
550+
},
551+
{
552+
name: "GitHub API repository URL with trailing path",
553+
url: "https://api.github.com/repos/owner/repo/pulls/1",
554+
expectedOwner: "owner",
555+
expectedRepo: "repo",
556+
},
557+
{
558+
name: "Simple owner/repo",
559+
url: "owner/repo",
560+
expectedOwner: "owner",
561+
expectedRepo: "repo",
562+
},
563+
{
564+
name: "No slashes - should error",
565+
url: "crash",
566+
expectError: true,
567+
},
568+
{
569+
name: "Empty string - should error",
570+
url: "",
571+
expectError: true,
572+
},
573+
{
574+
name: "Single segment path - should error",
575+
url: "https://api.github.com/repos/owner",
576+
expectError: true,
577+
},
578+
{
579+
name: "Enterprise GitHub API URL",
580+
url: "https://github.example.com/api/v3/repos/myorg/myrepo",
581+
expectedOwner: "myorg",
582+
expectedRepo: "myrepo",
583+
},
584+
{
585+
name: "Empty owner after repos segment",
586+
url: "https://api.github.com/repos//repo",
587+
expectError: true,
588+
},
589+
{
590+
name: "Empty repo after repos segment",
591+
url: "https://api.github.com/repos/owner/",
592+
expectError: true,
593+
},
594+
}
595+
for _, tc := range tests {
596+
t.Run(tc.name, func(t *testing.T) {
597+
owner, repo, err := getRepoOwnerAndNameFromURL(tc.url)
598+
if tc.expectError {
599+
assert.Error(t, err)
600+
assert.Empty(t, owner)
601+
assert.Empty(t, repo)
602+
} else {
603+
assert.NoError(t, err)
604+
assert.Equal(t, tc.expectedOwner, owner)
605+
assert.Equal(t, tc.expectedRepo, repo)
606+
}
607+
})
608+
}
609+
}
610+
517611
func TestUpdateSettings(t *testing.T) {
518612
mockKvStore, mockAPI, mockLogger, mockLoggerWith, _ := GetTestSetup(t)
519613
p := getPluginTest(mockAPI, mockKvStore)

0 commit comments

Comments
 (0)