From 78b7bbde07948049b916d76b4769faee461ff930 Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Mon, 4 May 2026 09:26:50 -0400 Subject: [PATCH] fix(validators): reject HTML metacharacters and whitespace in websiteUrl (#1249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `validateWebsiteURL` only checked that the URL parses, is absolute, and uses the `https` scheme. It accepted literal `"`, `'`, `<`, `>`, and ASCII space — none of which are valid in a URI per RFC 3986, and all of which broke rendering when the value flowed into the catalogue UI's `` template. Add a `strings.IndexAny` check after the scheme check that rejects any of these characters with a clear `website-url-invalid-characters` issue that points at the offending byte position. Control characters (`\t`, `\n`, `\r`) are also covered: they're caught one step earlier by Go's `url.Parse`, but the bytes are listed in the rejection set so the behaviour is explicit and survives any future relaxation of `url.Parse`. Already-percent-encoded URLs (e.g. `?q=hello%20world`) continue to validate cleanly. Adds table-driven cases for `"`, `'`, `<>`, space, newline, and a positive case for percent-encoded special characters. Co-Authored-By: Claude Opus 4.7 (1M context) Co-authored-by: Claude Opus 4.7 (1M context) --- internal/validators/validators.go | 14 ++++ internal/validators/validators_test.go | 94 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/internal/validators/validators.go b/internal/validators/validators.go index 4da756d..114ed3c 100644 --- a/internal/validators/validators.go +++ b/internal/validators/validators.go @@ -195,6 +195,20 @@ func validateWebsiteURL(ctx *ValidationContext, websiteURL string) *ValidationRe result.AddIssue(issue) } + // Reject characters that aren't valid in a URI per RFC 3986 and that have + // caused rendering issues when websiteUrl flows into the catalogue UI's + // href attributes. Publishers should percent-encode these in the source URL. + if i := strings.IndexAny(websiteURL, "\"'<> \t\n\r"); i >= 0 { + issue := NewValidationIssue( + ValidationIssueTypeSemantic, + ctx.String(), + fmt.Sprintf("websiteUrl contains an invalid character %q at position %d: %s", websiteURL[i], i, websiteURL), + ValidationIssueSeverityError, + "website-url-invalid-characters", + ) + result.AddIssue(issue) + } + return result } diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go index d4ff5c1..c3e5585 100644 --- a/internal/validators/validators_test.go +++ b/internal/validators/validators_test.go @@ -13,6 +13,8 @@ import ( "github.com/modelcontextprotocol/registry/pkg/model" ) +const websiteURLInvalidCharErrSubstr = "websiteUrl contains an invalid character" + func TestValidate(t *testing.T) { tests := []struct { name string @@ -486,6 +488,98 @@ func TestValidate(t *testing.T) { }, expectedError: "invalid websiteUrl:", }, + { + name: "server with websiteUrl containing double quote", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + WebsiteURL: `https://example.com/"oops`, + }, + expectedError: websiteURLInvalidCharErrSubstr, + }, + { + name: "server with websiteUrl containing single quote", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + WebsiteURL: "https://example.com/it's", + }, + expectedError: websiteURLInvalidCharErrSubstr, + }, + { + name: "server with websiteUrl containing angle brackets", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + WebsiteURL: "https://example.com/", + }, + expectedError: websiteURLInvalidCharErrSubstr, + }, + { + name: "server with websiteUrl containing space", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + WebsiteURL: "https://example.com/has space", + }, + expectedError: websiteURLInvalidCharErrSubstr, + }, + { + name: "server with websiteUrl containing newline", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + WebsiteURL: "https://example.com/has\nnewline", + }, + // url.Parse rejects ASCII control chars before our character-set + // check runs; either path produces a validation error. + expectedError: "invalid websiteUrl:", + }, + { + name: "server with websiteUrl with percent-encoded special chars is accepted", + serverDetail: apiv0.ServerJSON{ + Schema: model.CurrentSchemaURL, + Name: "com.example/test-server", + Description: "A test server", + Repository: &model.Repository{ + URL: "https://github.com/owner/repo", + Source: "github", + }, + Version: "1.0.0", + WebsiteURL: "https://example.com/path/?q=hello%20world&r=a%22b", + }, + expectedError: "", + }, { name: "server with websiteUrl that matches namespace domain", serverDetail: apiv0.ServerJSON{