From 6fb85ee32d84ac572cfe4fae00220782599aed06 Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Mon, 18 May 2026 23:19:20 +0000 Subject: [PATCH] fix: align frontend loopback allowlist with backend 127.0.0.0/8 range Co-Authored-By: Claude Opus 4.7 (1M context) --- .../(logged)/oauth/mcp_authorize/+page.svelte | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/(root)/(logged)/oauth/mcp_authorize/+page.svelte b/frontend/src/routes/(root)/(logged)/oauth/mcp_authorize/+page.svelte index 8b4f2b2a7a..7056fa6074 100644 --- a/frontend/src/routes/(root)/(logged)/oauth/mcp_authorize/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/oauth/mcp_authorize/+page.svelte @@ -20,7 +20,20 @@ let codeChallengeMethod = page.url.searchParams.get('code_challenge_method') || '' // Loopback hosts allowed over http per RFC 8252 ยง7.3 (native apps). - const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']) + // Must mirror the backend `validate_redirect_uri` exactly: `localhost`, + // the whole IPv4 `127.0.0.0/8` block (Rust `Ipv4Addr::is_loopback`), and + // IPv6 `::1` (which `new URL()` normalizes to the bracketed `[::1]`). + function isLoopbackHost(hostname: string): boolean { + if (hostname === 'localhost' || hostname === '[::1]') { + return true + } + const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname) + if (!m) { + return false + } + const octets = m.slice(1).map(Number) + return octets.every((o) => o <= 255) && octets[0] === 127 + } // Parse and scheme-check the redirect URI. `new URL()` alone is NOT a // safety check: `new URL('javascript:fetch(1)')` parses successfully with @@ -34,7 +47,7 @@ return null } const isHttps = url.protocol === 'https:' - const isLoopbackHttp = url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname) + const isLoopbackHttp = url.protocol === 'http:' && isLoopbackHost(url.hostname) return isHttps || isLoopbackHttp ? url : null }