fix(validators): reject HTML metacharacters and whitespace in websiteUrl (#1249)

`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
`<a href="...">` 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) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Radoslav Dimitrov 2026-05-04 09:26:50 -04:00 • committed by GitHub
commit 78b7bbde07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 108 additions and 0 deletions

View file

@ -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
}

View file

@ -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/<x>",
},
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{