From 61c41ef240a5207f1b88af5c200cfe3ae11c6b8f Mon Sep 17 00:00:00 2001 From: Voyvodka Date: Mon, 11 May 2026 09:57:40 +0300 Subject: [PATCH] fix(security): close js/incomplete-url-substring-sanitization in sample mock-fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL alert #11 (HIGH severity, js/incomplete-url-substring-sanitization) on samples/portal-host/src/mock-fetch.ts:117. The previous shape was: if (!url.startsWith(BASE)) return originalFetch(input, init); with BASE = 'https://hooks.example.com'. The startsWith check correctly catches paths like https://hooks.example.com/api/v1/portal/endpoints, but it ALSO matches https://hooks.example.com.attacker.com/api/v1/portal/endpoints — the BASE prefix can be followed by an arbitrary host suffix. This is fine for the sample's mock semantics in isolation (the worst case is the mock answers a request that should have gone to the real network), but the kind of pattern that absolutely should not get copy-pasted into production code. Fixed by parsing the URL and comparing protocol + host explicitly via WHATWG URL. Same intent, no substring trap. Also flipped path = url.slice(BASE.length).split('?')[0] to path = parsed.pathname which is the same value via a safer route. The route handlers downstream are unchanged. Closes the CodeQL alert. No test changes required — the mock continues to serve the same routes for the same inputs the sample emits. --- samples/portal-host/src/mock-fetch.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/samples/portal-host/src/mock-fetch.ts b/samples/portal-host/src/mock-fetch.ts index 4b0f03d..f53ca47 100644 --- a/samples/portal-host/src/mock-fetch.ts +++ b/samples/portal-host/src/mock-fetch.ts @@ -108,15 +108,28 @@ function notFound(): Response { } const BASE = "https://hooks.example.com"; +const BASE_URL = new URL(BASE); const originalFetch = globalThis.fetch; export function installMockFetch(): void { globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; - if (!url.startsWith(BASE)) return originalFetch(input, init); + // Parse the URL and compare the host explicitly. A naive + // url.startsWith(BASE) check would also match + // https://hooks.example.com.attacker.com/... — fine for a sample + // but the kind of pattern that gets copy-pasted into production code. + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return originalFetch(input, init); + } + if (parsed.protocol !== BASE_URL.protocol || parsed.host !== BASE_URL.host) { + return originalFetch(input, init); + } - const path = url.slice(BASE.length).split("?")[0]; + const path = parsed.pathname; const method = (init?.method ?? "GET").toUpperCase(); // GET /api/v1/portal/endpoints