GHSA-RQV2-M695-F8J4

Vulnerability from github – Published: 2026-05-08 17:18 – Updated: 2026-05-08 17:18
VLAI?
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 textContentinnerHTML 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.govalidateWebsiteURL (lines 153–199)
  • Sink: internal/api/handlers/v0/ui_index.htmltoggleDetails(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)

  1. 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

  1. 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 accepts literal " in the path component:

input="https://example.com/\"onmouseover=alert(1)//"  IsAbs=true  Scheme="https"  Path="/\"onmouseover=alert(1)//"

Neither the Huma format:"uri" annotation nor validateWebsiteURL's scheme/IsAbs triplet rejects this string. The architecture's existing protection — repository.url is regex-locked to ^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$ and therefore cannot contain quotes — does not extend to websiteUrl, which has no allowlist.

Why client-side escapeHtml does not catch this

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

Per the HTML5 spec (§13.3 Serialising HTML fragments), the only characters encoded inside the text content of an element are &, <, >, and U+00A0. " and ' are not encoded because in a text-content context they are not special. The helper is therefore safe in element-text contexts (where it is correctly used for name, version, description, etc.) but unsafe inside an attribute value, which is precisely where it is invoked for href on lines 432 and 426.

Impact

  • Stored XSS on the official MCP Registry homepage. The malicious entry sits in the public catalogue alongside legitimate ones; any user expanding the entry triggers the payload.
  • Because the page is served on the official registry.modelcontextprotocol.io origin, the injected script can:
  • Read and overwrite localStorage (baseUrl, customUrl), pinning the user's subsequent reads to an attacker-controlled "Custom" base URL.
  • Issue any same-origin or cross-origin XHR (connect-src * is granted).
  • Phish for Registry JWTs by injecting fake auth flows on the trusted origin.
  • The CSP script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com does not block this because 'unsafe-inline' permits inline event-handler attributes.

Suggested remediation (any one suffices)

  1. Replace the homegrown escapeHtml with an attribute-safe encoder that also escapes ", ', backtick, and = — the OWASP HTML attribute-encoding rule.
  2. Avoid building the href via string templates. Use setAttribute('href', value) instead — setAttribute is not subject to HTML tokenisation, so no breakout is possible.
  3. Tighten validateWebsiteURL to reject any URL whose raw bytes contain ", ', <, >, , \t, or \n, or — conservatively — store the canonical re-serialised form (parsedURL.String() percent-encodes such characters in the path).
  4. Drop 'unsafe-inline' from script-src after auditing the inline scripts on the page.

Approach (3) is the smallest server-side change and immediately neutralises the exploit for any new publishes; approaches (1) or (2) close the class of bug at the sink so future fields with similar patterns are safe by default.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/modelcontextprotocol/registry"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.7.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44429"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79",
      "CWE-116"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-08T17:18:32Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe 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` \u2192 `innerHTML` round-trip. Per the HTML serialisation algorithm, that round-trip encodes only `\u0026`, `\u003c`, `\u003e` and U+00A0 inside text nodes \u2014 it does **not** encode `\"` or `\u0027`. A literal `\"` in `websiteUrl` therefore breaks out of the `href` attribute, allowing arbitrary `on*` event handlers to be appended to the same `\u003ca\u003e` element. The Content-Security-Policy on `/` is `script-src \u0027self\u0027 \u0027unsafe-inline\u0027 https://cdn.tailwindcss.com`, so the injected event handlers execute.\n\nAny 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.\n\n## Affected component\n\n- Validator: `internal/validators/validators.go` \u2014 `validateWebsiteURL` (lines 153\u2013199)\n- Sink: `internal/api/handlers/v0/ui_index.html` \u2014 `toggleDetails(card, item)` at line 432, the `href` attribute built around `escapeHtml(server.websiteUrl)`\n- Helper: `escapeHtml` defined at `internal/api/handlers/v0/ui_index.html` lines 494\u2013498\n\n## Proof of concept\n\n1. Obtain a Registry JWT for any namespace you control (a GitHub OAuth exchange against a throwaway account suffices):\n\n   ```bash\n   TOKEN=$(curl -sS -X POST https://registry.modelcontextprotocol.io/v0/auth/github-at \\\n        -H \u0027Content-Type: application/json\u0027 \\\n        -d \u0027{\"github_token\":\"\u003cgh-pat\u003e\"}\u0027 | jq -r .registry_token)\n   ```\n\n2. Publish a server with a poisoned `websiteUrl`. The literal `\"` is preserved end-to-end:\n\n   ```bash\n   curl -sS -X POST https://registry.modelcontextprotocol.io/v0/publish \\\n     -H \"Authorization: Bearer $TOKEN\" \\\n     -H \u0027Content-Type: application/json\u0027 \\\n     --data-binary @- \u003c\u003c\u0027EOF\u0027\n   {\n     \"$schema\": \"https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json\",\n     \"name\":  \"io.github.\u003cyour-account\u003e/xss-poc\",\n     \"version\": \"0.0.1\",\n     \"description\": \"hover the website link\",\n     \"websiteUrl\": \"https://example.com/\\\"onmouseover=alert(document.domain)//\"\n   }\n   EOF\n   ```\n\n3. 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.\n\n## Why server-side validation does not catch this\n\nGo\u0027s `net/url.Parse` accepts literal `\"` in the path component:\n\n```\ninput=\"https://example.com/\\\"onmouseover=alert(1)//\"  IsAbs=true  Scheme=\"https\"  Path=\"/\\\"onmouseover=alert(1)//\"\n```\n\nNeither the Huma `format:\"uri\"` annotation nor `validateWebsiteURL`\u0027s scheme/`IsAbs` triplet rejects this string. The architecture\u0027s existing protection \u2014 `repository.url` is regex-locked to `^https?://(www\\.)?github\\.com/[\\w.-]+/[\\w.-]+/?$` and therefore cannot contain quotes \u2014 does not extend to `websiteUrl`, which has no allowlist.\n\n## Why client-side `escapeHtml` does not catch this\n\n```js\nfunction escapeHtml(text) {\n    const div = document.createElement(\u0027div\u0027);\n    div.textContent = text;\n    return div.innerHTML;\n}\n```\n\nPer the HTML5 spec (\u00a713.3 Serialising HTML fragments), the only characters encoded inside the text content of an element are `\u0026`, `\u003c`, `\u003e`, and U+00A0. `\"` and `\u0027` are **not** encoded because in a text-content context they are not special. The helper is therefore safe in element-text contexts (where it is correctly used for `name`, `version`, `description`, etc.) but unsafe inside an attribute value, which is precisely where it is invoked for `href` on lines 432 and 426.\n\n## Impact\n\n- Stored XSS on the official MCP Registry homepage. The malicious entry sits in the public catalogue alongside legitimate ones; any user expanding the entry triggers the payload.\n- Because the page is served on the official `registry.modelcontextprotocol.io` origin, the injected script can:\n  - Read and overwrite `localStorage` (`baseUrl`, `customUrl`), pinning the user\u0027s subsequent reads to an attacker-controlled \"Custom\" base URL.\n  - Issue any same-origin or cross-origin XHR (`connect-src *` is granted).\n  - Phish for Registry JWTs by injecting fake auth flows on the trusted origin.\n- The CSP `script-src \u0027self\u0027 \u0027unsafe-inline\u0027 https://cdn.tailwindcss.com` does not block this because `\u0027unsafe-inline\u0027` permits inline event-handler attributes.\n\n## Suggested remediation (any one suffices)\n\n1. Replace the homegrown `escapeHtml` with an attribute-safe encoder that also escapes `\"`, `\u0027`, backtick, and `=` \u2014 the OWASP HTML attribute-encoding rule.\n2. Avoid building the `href` via string templates. Use `setAttribute(\u0027href\u0027, value)` instead \u2014 `setAttribute` is not subject to HTML tokenisation, so no breakout is possible.\n3. Tighten `validateWebsiteURL` to reject any URL whose raw bytes contain `\"`, `\u0027`, `\u003c`, `\u003e`, ` `, `\\t`, or `\\n`, or \u2014 conservatively \u2014 store the canonical re-serialised form (`parsedURL.String()` percent-encodes such characters in the path).\n4. Drop `\u0027unsafe-inline\u0027` from `script-src` after auditing the inline scripts on the page.\n\nApproach (3) is the smallest server-side change and immediately neutralises the exploit for any new publishes; approaches (1) or (2) close the class of bug at the sink so future fields with similar patterns are safe by default.",
  "id": "GHSA-rqv2-m695-f8j4",
  "modified": "2026-05-08T17:18:32Z",
  "published": "2026-05-08T17:18:32Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/registry/security/advisories/GHSA-rqv2-m695-f8j4"
    },
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/registry/pull/1249"
    },
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/registry/commit/78b7bbde07948049b916d76b4769faee461ff930"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/modelcontextprotocol/registry"
    },
    {
      "type": "WEB",
      "url": "https://github.com/modelcontextprotocol/registry/releases/tag/v1.7.7"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:N/SI:L/SA:L",
      "type": "CVSS_V4"
    }
  ],
  "summary": "MCP Registry vulnerable to stored XSS in catalogue UI via attribute-quote breakout in publisher-controlled `websiteUrl`"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…
Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…