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 {