GitHub Security Advisory: GHSA-rqv2-m695-f8j4
Package: github.com/modelcontextprotocol/registry (GO)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), CWE-116: Improper Encoding or Escaping of Output
Vulnerable versions: < 1.7.7
Patched in: 1.7.7

Summary: MCP Registry vulnerable to stored XSS in catalogue UI via attribute-quote breakout in publisher-controlled `websiteUrl`

Details:
## Summary

The public catalogue UI served at `GET /` (file `internal/api/handlers/v0/ui_index.html`) is vulnerable to stored cross-site scripting via the `server.websiteUrl` field of any published `server.json`. Server-side validation in `internal/validators/validators.go` (`validateWebsiteURL`) only checks that the URL parses, is absolute, and uses the `https` scheme; it does not reject quote characters. Client-side, the value is interpolated into a double-quoted `href` attribute via `innerHTML`, using a homegrown `escapeHtml` helper that performs the standard `textContent` → `innerHTML` round-trip. Per the HTML serialisation algorithm, that round-trip encodes only `&`, `<`, `>` and U+00A0 inside text nodes — it does **not** encode `"` or `'`. A literal `"` in `websiteUrl` therefore breaks out of the `href` attribute, allowing arbitrary `on*` event handlers to be appended to the same `<a>` element. The Content-Security-Policy on `/` is `script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com`, so the injected event handlers execute.

Any user able to obtain a publish token (e.g. via `POST /v0/auth/github-at` with their own GitHub account, or `POST /v0/auth/none` on a deployment that has anonymous auth enabled) can plant a poisoned record visible to every visitor of the registry homepage.

## Affected component

- Validator: `internal/validators/validators.go` — `validateWebsiteURL` (lines 153–199)
- Sink: `internal/api/handlers/v0/ui_index.html` — `toggleDetails(card, item)` at line 432, the `href` attribute built around `escapeHtml(server.websiteUrl)`
- Helper: `escapeHtml` defined at `internal/api/handlers/v0/ui_index.html` lines 494–498

## Proof of concept

1. Obtain a Registry JWT for any namespace you control (a GitHub OAuth exchange against a throwaway account suffices):

   ```bash
   TOKEN=$(curl -sS -X POST https://registry.modelcontextprotocol.io/v0/auth/github-at \
        -H 'Content-Type: application/json' \
        -d '{"github_token":"<gh-pat>"}' | jq -r .registry_token)
   ```

2. Publish a server with a poisoned `websiteUrl`. The literal `"` is preserved end-to-end:

   ```bash
   curl -sS -X POST https://registry.modelcontextprotocol.io/v0/publish \
     -H "Authorization: Bearer $TOKEN" \
     -H 'Content-Type: application/json' \
     --data-binary @- <<'EOF'
   {
     "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
     "name":  "io.github.<your-account>/xss-poc",
     "version": "0.0.1",
     "description": "hover the website link",
     "websiteUrl": "https://example.com/\"onmouseover=alert(document.domain)//"
   }
   EOF
   ```

3. Visit `https://registry.modelcontextprotocol.io/`, search for `xss-poc`, click the card to expand it, then hover the **Website** link in the details panel. The injected `onmouseover` fires and `alert(document.domain)` runs on the `registry.modelcontextprotocol.io` origin.

## Why server-side validation does not catch this

Go's `net/url.Parse`