Skip to content

Commit 78b7bbd

Browse files
rdimitrovclaude
andauthored
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>
1 parent fbfd63c commit 78b7bbd

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

internal/validators/validators.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,20 @@ func validateWebsiteURL(ctx *ValidationContext, websiteURL string) *ValidationRe
195195
result.AddIssue(issue)
196196
}
197197

198+
// Reject characters that aren't valid in a URI per RFC 3986 and that have
199+
// caused rendering issues when websiteUrl flows into the catalogue UI's
200+
// href attributes. Publishers should percent-encode these in the source URL.
201+
if i := strings.IndexAny(websiteURL, "\"'<> \t\n\r"); i >= 0 {
202+
issue := NewValidationIssue(
203+
ValidationIssueTypeSemantic,
204+
ctx.String(),
205+
fmt.Sprintf("websiteUrl contains an invalid character %q at position %d: %s", websiteURL[i], i, websiteURL),
206+
ValidationIssueSeverityError,
207+
"website-url-invalid-characters",
208+
)
209+
result.AddIssue(issue)
210+
}
211+
198212
return result
199213
}
200214

internal/validators/validators_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/modelcontextprotocol/registry/pkg/model"
1414
)
1515

16+
const websiteURLInvalidCharErrSubstr = "websiteUrl contains an invalid character"
17+
1618
func TestValidate(t *testing.T) {
1719
tests := []struct {
1820
name string
@@ -486,6 +488,98 @@ func TestValidate(t *testing.T) {
486488
},
487489
expectedError: "invalid websiteUrl:",
488490
},
491+
{
492+
name: "server with websiteUrl containing double quote",
493+
serverDetail: apiv0.ServerJSON{
494+
Schema: model.CurrentSchemaURL,
495+
Name: "com.example/test-server",
496+
Description: "A test server",
497+
Repository: &model.Repository{
498+
URL: "https://github.com/owner/repo",
499+
Source: "github",
500+
},
501+
Version: "1.0.0",
502+
WebsiteURL: `https://example.com/"oops`,
503+
},
504+
expectedError: websiteURLInvalidCharErrSubstr,
505+
},
506+
{
507+
name: "server with websiteUrl containing single quote",
508+
serverDetail: apiv0.ServerJSON{
509+
Schema: model.CurrentSchemaURL,
510+
Name: "com.example/test-server",
511+
Description: "A test server",
512+
Repository: &model.Repository{
513+
URL: "https://github.com/owner/repo",
514+
Source: "github",
515+
},
516+
Version: "1.0.0",
517+
WebsiteURL: "https://example.com/it's",
518+
},
519+
expectedError: websiteURLInvalidCharErrSubstr,
520+
},
521+
{
522+
name: "server with websiteUrl containing angle brackets",
523+
serverDetail: apiv0.ServerJSON{
524+
Schema: model.CurrentSchemaURL,
525+
Name: "com.example/test-server",
526+
Description: "A test server",
527+
Repository: &model.Repository{
528+
URL: "https://github.com/owner/repo",
529+
Source: "github",
530+
},
531+
Version: "1.0.0",
532+
WebsiteURL: "https://example.com/<x>",
533+
},
534+
expectedError: websiteURLInvalidCharErrSubstr,
535+
},
536+
{
537+
name: "server with websiteUrl containing space",
538+
serverDetail: apiv0.ServerJSON{
539+
Schema: model.CurrentSchemaURL,
540+
Name: "com.example/test-server",
541+
Description: "A test server",
542+
Repository: &model.Repository{
543+
URL: "https://github.com/owner/repo",
544+
Source: "github",
545+
},
546+
Version: "1.0.0",
547+
WebsiteURL: "https://example.com/has space",
548+
},
549+
expectedError: websiteURLInvalidCharErrSubstr,
550+
},
551+
{
552+
name: "server with websiteUrl containing newline",
553+
serverDetail: apiv0.ServerJSON{
554+
Schema: model.CurrentSchemaURL,
555+
Name: "com.example/test-server",
556+
Description: "A test server",
557+
Repository: &model.Repository{
558+
URL: "https://github.com/owner/repo",
559+
Source: "github",
560+
},
561+
Version: "1.0.0",
562+
WebsiteURL: "https://example.com/has\nnewline",
563+
},
564+
// url.Parse rejects ASCII control chars before our character-set
565+
// check runs; either path produces a validation error.
566+
expectedError: "invalid websiteUrl:",
567+
},
568+
{
569+
name: "server with websiteUrl with percent-encoded special chars is accepted",
570+
serverDetail: apiv0.ServerJSON{
571+
Schema: model.CurrentSchemaURL,
572+
Name: "com.example/test-server",
573+
Description: "A test server",
574+
Repository: &model.Repository{
575+
URL: "https://github.com/owner/repo",
576+
Source: "github",
577+
},
578+
Version: "1.0.0",
579+
WebsiteURL: "https://example.com/path/?q=hello%20world&r=a%22b",
580+
},
581+
expectedError: "",
582+
},
489583
{
490584
name: "server with websiteUrl that matches namespace domain",
491585
serverDetail: apiv0.ServerJSON{

0 commit comments

Comments
 (0)