feat(publish): escape server name to prevent HTML injection attacks
This commit is contained in:
parent
022dbab85d
commit
820e0ab8b9
3 changed files with 79 additions and 1 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/<script>alert('XSS')</script>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/<script>alert('XSS')</script>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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue