Merge pull request #2326 from modelcontextprotocol/v2/fix/2305-cimd-loopback-http

fix: exempt loopback hosts from the CIMD metadata URL HTTPS requirement
This commit is contained in:
Cliff Hall 2026-09-11 10:22:31 -04:00 • committed by GitHub
commit 336e045210
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -58,9 +58,63 @@ const HttpUrlStringSchema = z
export const CIMD_METADATA_URL_INVALID_ERROR =
"Must be a valid URL, like https://example.com/oauth/client.json";
/** Field-level error when a CIMD metadata URL is not HTTPS. */
/**
* Field-level error when a CIMD metadata URL is neither HTTPS nor served from
* one of the exempt loopback literals. The message names the exemption rather
* than saying only "must use HTTPS", so a developer pointing the field at a
* local fixture is told which spellings work instead of concluding that nothing
* local can (the same phrasing lesson as the token-endpoint notice in
* `core/auth/oauthUx.ts`).
*/
export const CIMD_METADATA_URL_HTTPS_ERROR =
"CIMD client metadata URL must use HTTPS";
"CIMD client metadata URL must use HTTPS, except on localhost, 127.0.0.1 or [::1]";
/**
* Hosts for which a plain `http:` CIMD metadata URL is accepted.
*
* SEP-991 expects HTTPS in production for a real reason: a CIMD `client_id` is
* a URL the authorization server dereferences, and over plain HTTP the document
* it gets back is attacker-modifiable in transit. Loopback is the documented
* exception to exactly that reasoning — there is no network segment to sit on —
* which is why the SDK exempts these same three literals from its token-endpoint
* TLS assertion, and why `core/auth/cimd.ts` already notes that an
* already-stored `client_id` may be an `http://` URL "used by local dev/test
* metadata servers". Without this, the runtime tolerated a value the config
* validation would not let anyone enter, and CIMD could not be driven against
* any fixture in this repo (#2305).
*
* ⚠️ This is the SDK's own list, matched member for member — not the broader
* `isLoopbackHost` in `core/node/hostUrl.ts`, which imports `node:net` and so
* cannot be reached from this browser-safe module. The SDK's
* `assertSecureTokenEndpoint` tests `url.hostname` against exactly
* `localhost` / `127.0.0.1` / `[::1]` (plus a `::1` arm `URL.hostname` never
* produces, since it brackets IPv6 literals). Keeping the two identical is what
* stops them disagreeing about one URL.
*
* ⚠️ **The comparison is against the WHATWG-canonicalized `URL.hostname`, so it
* covers alternate spellings of these three *addresses*, not three input
* strings.** `http://127.1/…`, `http://2130706433/…`, `http://0x7f.0.0.1/…` and
* `http://[0:0:0:0:0:0:0:1]/…` all canonicalize into the set and are accepted;
* so is a root-anchored `http://127.0.0.1./…`, which WHATWG strips the dot from
* for IP literals but *not* for `localhost.`. That is deliberate on both counts:
* the security argument is about the address the request actually reaches, and
* every one of these is the loopback interface. It is also exactly what the SDK
* does — it canonicalizes through `new URL` the same way — so an alias accepted
* here is an alias the SDK accepts too.
*/
const CIMD_HTTP_EXEMPT_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
/**
* True when a plain-`http:` CIMD metadata URL on this host is acceptable. Takes
* the already-canonicalized `URL.hostname`, never a raw authority or a
* `host:port` — passing an unparsed string would compare the wrong thing and
* silently reject every alias above. Not exported: the only caller is the
* validator below, and the exemption is exercised through it rather than
* through a second public surface that could drift from it.
*/
function isCimdHttpExemptHost(hostname: string): boolean {
return CIMD_HTTP_EXEMPT_HOSTS.has(hostname);
}
/** Field-level error when a CIMD metadata URL has no path segment. */
export const CIMD_METADATA_URL_PATH_ERROR =
@ -81,7 +135,7 @@ export function getCimdClientMetadataUrlError(
}
try {
const url = new URL(trimmed);
if (url.protocol !== "https:") {
if (url.protocol !== "https:" && !isCimdHttpExemptHost(url.hostname)) {
return CIMD_METADATA_URL_HTTPS_ERROR;
}
if (url.pathname === "/" || url.pathname === "") {