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:
parent
fbfd63ced8
commit
78b7bbde07
2 changed files with 108 additions and 0 deletions
|
|
@ -195,6 +195,20 @@ func validateWebsiteURL(ctx *ValidationContext, websiteURL string) *ValidationRe
|
||||||
result.AddIssue(issue)
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"github.com/modelcontextprotocol/registry/pkg/model"
|
"github.com/modelcontextprotocol/registry/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const websiteURLInvalidCharErrSubstr = "websiteUrl contains an invalid character"
|
||||||
|
|
||||||
func TestValidate(t *testing.T) {
|
func TestValidate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
@ -486,6 +488,98 @@ func TestValidate(t *testing.T) {
|
||||||
},
|
},
|
||||||
expectedError: "invalid websiteUrl:",
|
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",
|
name: "server with websiteUrl that matches namespace domain",
|
||||||
serverDetail: apiv0.ServerJSON{
|
serverDetail: apiv0.ServerJSON{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue