diff --git a/internal/api/handlers/v0/publish.go b/internal/api/handlers/v0/publish.go index dfc57cd..cbbe04a 100644 --- a/internal/api/handlers/v0/publish.go +++ b/internal/api/handlers/v0/publish.go @@ -12,6 +12,7 @@ import ( "github.com/modelcontextprotocol/registry/internal/database" "github.com/modelcontextprotocol/registry/internal/model" "github.com/modelcontextprotocol/registry/internal/service" + "golang.org/x/net/html" ) // PublishHandler handles requests to publish new server details to the registry @@ -83,11 +84,13 @@ func PublishHandler(registry service.RegistryService, authService auth.Service) authMethod = model.AuthMethodNone } + serverName := html.EscapeString(serverDetail.Name) + // Setup authentication info a := model.Authentication{ Method: authMethod, Token: token, - RepoRef: serverDetail.Name, + RepoRef: serverName, } valid, err := authService.ValidateAuth(r.Context(), a) diff --git a/internal/api/handlers/v0/publish_test.go b/internal/api/handlers/v0/publish_test.go index 214579a..641e730 100644 --- a/internal/api/handlers/v0/publish_test.go +++ b/internal/api/handlers/v0/publish_test.go @@ -283,6 +283,80 @@ func TestPublishHandler(t *testing.T) { expectedStatus: http.StatusInternalServerError, expectedError: "Failed to publish server details:", }, + { + name: "HTML injection attack in name field", + method: http.MethodPost, + requestBody: model.ServerDetail{ + Server: model.Server{ + ID: "test-id-html", + Name: "io.github.malicious/test-server", + Description: "A test server with HTML injection attempt", + Repository: model.Repository{ + URL: "https://github.com/malicious/test-server", + Source: "github", + ID: "malicious/test-server", + }, + VersionDetail: model.VersionDetail{ + Version: "1.0.0", + ReleaseDate: "2025-05-25T00:00:00Z", + IsLatest: true, + }, + }, + }, + authHeader: "Bearer github_token_123", + setupMocks: func(registry *MockRegistryService, authSvc *MockAuthService) { + // The auth service should receive the escaped HTML version of the name + authSvc.Mock.On("ValidateAuth", mock.Anything, mock.MatchedBy(func(auth model.Authentication) bool { + // Verify that the RepoRef contains escaped HTML, not the raw script tag + return auth.Method == model.AuthMethodGitHub && + auth.Token == "github_token_123" && + auth.RepoRef == "io.github.malicious/<script>alert('XSS')</script>test-server" + })).Return(true, nil) + registry.Mock.On("Publish", mock.AnythingOfType("*model.ServerDetail")).Return(nil) + }, + expectedStatus: http.StatusCreated, + expectedResponse: map[string]string{ + "message": "Server publication successful", + "id": "test-id-html", + }, + }, + { + name: "HTML injection attack in name field with non-GitHub prefix", + method: http.MethodPost, + requestBody: model.ServerDetail{ + Server: model.Server{ + ID: "test-id-html-non-github", + Name: "malicious.com/test-server", + Description: "A test server with HTML injection attempt (non-GitHub)", + Repository: model.Repository{ + URL: "https://malicious.com/test-server", + Source: "custom", + ID: "malicious/test-server", + }, + VersionDetail: model.VersionDetail{ + Version: "1.0.0", + ReleaseDate: "2025-05-25T00:00:00Z", + IsLatest: true, + }, + }, + }, + authHeader: "Bearer some_token", + setupMocks: func(registry *MockRegistryService, authSvc *MockAuthService) { + // The auth service should receive the escaped HTML version of the name with AuthMethodNone + authSvc.Mock.On("ValidateAuth", mock.Anything, mock.MatchedBy(func(auth model.Authentication) bool { + // Verify that the RepoRef contains escaped HTML, not the raw script tag + return auth.Method == model.AuthMethodNone && + auth.Token == "some_token" && + auth.RepoRef == "malicious.com/<script>alert('XSS')</script>test-server" + })).Return(true, nil) + registry.Mock.On("Publish", mock.AnythingOfType("*model.ServerDetail")).Return(nil) + }, + expectedStatus: http.StatusCreated, + expectedResponse: map[string]string{ + "message": "Server publication successful", + "id": "test-id-html-non-github", + }, + }, } for _, tc := range testCases { diff --git a/internal/auth/github.go b/internal/auth/github.go index ac81303..fc57ae1 100644 --- a/internal/auth/github.go +++ b/internal/auth/github.go @@ -225,6 +225,7 @@ func (g *GitHubDeviceAuth) checkOrgMembership(ctx context.Context, token, userna // GitHub API endpoint: GET /orgs/{org}/members/{username} // true if status code is 204 No Content // false if status code is 404 Not Found + url := fmt.Sprint("https://api.github.com/orgs/", org, "/members/", username) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil {