@@ -226,10 +226,41 @@ function pathMatches(a: string, b: string): boolean {
226226 return strip ( a ) === strip ( b )
227227}
228228
229+ /**
230+ * Whether `hostname` names a loopback host: `localhost` (or any `*.localhost`
231+ * subdomain), the IPv6 loopback `::1`, or an IPv4 literal inside the
232+ * `127.0.0.0/8` loopback block.
233+ *
234+ * The IPv4 case is matched **structurally** — the whole hostname must be a
235+ * canonical dotted-decimal IPv4 literal whose first octet is `127`. A bare
236+ * `startsWith('127.')` prefix check would also accept an attacker-controlled
237+ * DNS name that merely *begins* with `127.` (`127.attacker.example`,
238+ * `127.0.0.1.attacker.example`), letting a cross-origin browser page defeat
239+ * the loopback origin gate that guards the RPC/MCP surface (a DNS-rebinding /
240+ * cross-site WebSocket-hijacking bypass). Requiring a real IPv4 literal keeps
241+ * genuine loopback addresses (`127.0.0.1`, `127.5.5.5`) allowed while rejecting
242+ * those DNS names.
243+ */
229244export function isLoopbackHostname ( hostname : string ) : boolean {
230245 const h = hostname . replace ( / ^ \[ | \] $ / g, '' ) // strip IPv6 brackets
231- return h === 'localhost' || h === '127.0.0.1' || h === '::1'
232- || h . endsWith ( '.localhost' ) || h . startsWith ( '127.' )
246+ if ( h === 'localhost' || h . endsWith ( '.localhost' ) || h === '::1' )
247+ return true
248+ return isLoopbackIPv4 ( h )
249+ }
250+
251+ /** A canonical dotted-decimal IPv4 literal in `127.0.0.0/8`. */
252+ function isLoopbackIPv4 ( hostname : string ) : boolean {
253+ const octets = hostname . split ( '.' )
254+ if ( octets . length !== 4 || ! octets . every ( isDecimalOctet ) )
255+ return false
256+ return Number ( octets [ 0 ] ) === 127
257+ }
258+
259+ /** A single canonical IPv4 octet: 1–3 digits, no leading zero, value 0–255. */
260+ function isDecimalOctet ( part : string ) : boolean {
261+ if ( ! / ^ \d { 1 , 3 } $ / . test ( part ) || ( part . length > 1 && part [ 0 ] === '0' ) )
262+ return false
263+ return Number ( part ) <= 255
233264}
234265
235266/**
0 commit comments