client: sanitizeUrl rejects valid RFC 3986 bracketed IPv6 hostnames and double-encodes path/hash components
Description
In @mcp-use/client, the URL sanitization utility sanitizeUrl in packages/client/src/auth/url.ts is used to validate and sanitize OAuth redirect URIs and authorization URLs (e.g. in OAuthSessionStore and useMcp).
Two defects exist in this utility:
Rejection of Valid RFC 3986 / WHATWG IPv6 Bracketed Hostnames: Under RFC 3986 Section 3.2.2 and the WHATWG URL standard, IPv6 host literals are bracketed (e.g.,
http://[::1]:33418/callbackorhttps://[2001:db8::1]/auth). However,sanitizeUrlperforms the following hostname validation check:if (url.hostname !== encodeURIComponent(url.hostname)) abort();Because
encodeURIComponent("[::1]")produces"%5B%3A%3A1%5D",url.hostname !== encodeURIComponent(url.hostname)always evaluates totruefor all IPv6 addresses. As a result, any OAuth loopback or remote endpoint hosted on an IPv6 address (including[::1]) throws:Error: Invalid url to pass to open(): http://[::1]:33418/callbackDouble Percent-Encoding of Path Segments and Fragment Identifiers:
new URL(raw)already normalizes and percent-encodes paths. However,sanitizeUrlreappliesencodeURIComponenton path segments and the URL hash:url.pathname = url.pathname.slice(0, 1) + encodeURIComponent(url.pathname.slice(1)).replace(/%2f/gi, "/"); // ... url.hash = url.hash.slice(0, 1) + encodeURIComponent(url.hash.slice(1));Calling
encodeURIComponenton an already percent-encoded string converts existing%characters into%25. For example:/api/tenant%201/callbackbecomes/api/tenant%25201/callback(double-encoded space)./docs/C%2B%2Bbecomes/docs/C%252B%252B(double-encoded+)./user%2Fprofilebecomes/user%2Fprofileinitially or double-encodes other reserved sequences.#/routes/profilebecomes#%2Froutes%2Fprofilein hash fragments, breaking single-page application (SPA) client routing.
Steps to Reproduce
import { sanitizeUrl } from "@mcp-use/client/auth/url";
// Defect 1: IPv6 rejection
sanitizeUrl("http://[::1]:33418/callback");
// Throws: Error: Invalid url to pass to open(): http://[::1]:33418/callback
// Defect 2: Double percent-encoding
const sanitized = sanitizeUrl("https://example.com/api/tenant%201/callback");
console.log(sanitized);
// Outputs: "https://example.com/api/tenant%25201/callback" (Expected: "https://example.com/api/tenant%201/callback")
const spaRoute = sanitizeUrl("https://example.com/app#/routes/settings");
console.log(spaRoute);
// Outputs: "https://example.com/app#%2Froutes%2Fsettings" (Expected: "https://example.com/app#/routes/settings")Expected Behavior
- Bracketed IPv6 hostnames adhering to RFC 3986 / WHATWG standards (such as
[::1]or[2001:db8::1]) should be recognized as valid hostnames. - Valid percent-encoded octets (
%[0-9a-fA-F]{2}) in paths, query parameters, credentials, and fragments should be preserved without double-encoding. - Fragment paths containing
/(common in SPA routes) should not have their slashes percent-encoded. - Malicious protocols (
javascript:,data:,file:) and unsafe hostnames with control characters or whitespace should continue to be strictly rejected.
Suggested Fix
- Enhance hostname validation to allow bracketed IPv6 literals matching
/^[0-9a-fA-F:.]+$/within[and]:function isValidHostname(hostname: string): boolean { if (hostname.startsWith("[") && hostname.endsWith("]")) { const ipv6 = hostname.slice(1, -1); return ipv6.length > 0 && /^[0-9a-fA-F:.]+$/.test(ipv6); } return hostname === encodeURIComponent(hostname); } - Sanitize path segments and fragments by preserving existing valid
%XXoctets and only encoding unescaped characters:function sanitizeEncodedComponent( value: string, encodeFn: (s: string) => string = encodeURIComponent ): string { return value.replace( /(%[0-9a-fA-F]{2})|([^%]+)|(%)/g, (_match, pct, plain, roguePct) => { if (pct) return pct; if (plain) return encodeFn(plain); if (roguePct) return "%25"; return _match; } ); }
Source: mcp-use/mcp-use