{"record":{"id":"3d5e61decdc6fada","repo":"decolua/9router","slug":"blocked-url-internal-host","errorCode":null,"errorMessage":"Blocked URL: internal host","messagePattern":"Blocked URL: internal host","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"src/shared/utils/ssrfGuard.js","lineNumber":52,"sourceCode":"    const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;\n    return (ip & mask) === (base & mask);\n  });\n}\n\nfunction isBlockedIpv6(host) {\n  const h = host.replace(/^\\[|\\]$/g, \"\").toLowerCase();\n  const v4Mapped = h.match(/^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n  if (v4Mapped) return isBlockedIpv4(v4Mapped[1]);\n  if (h === \"::1\" || h === \"::\") return true;\n  return h.startsWith(\"fe80:\") || h.startsWith(\"fc\") || h.startsWith(\"fd\");\n}\n\n// Throw if URL targets a non-public host. Caller should map to 400.\nexport function assertPublicUrl(rawUrl) {\n  const parsed = new URL(rawUrl);\n  const host = parsed.hostname.toLowerCase();\n\n  if (BLOCKED_HOSTNAMES.has(host)) throw new Error(\"Blocked URL: internal host\");\n  if (BLOCKED_SUFFIXES.some((s) => host.endsWith(s))) throw new Error(\"Blocked URL: internal host\");\n  if (isBlockedIpv4(host)) throw new Error(\"Blocked URL: private IP\");\n  if (host.includes(\":\") && isBlockedIpv6(host)) throw new Error(\"Blocked URL: private IP\");\n}\n","sourceCodeStart":34,"sourceCodeEnd":57,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/shared/utils/ssrfGuard.js#L34-L57","documentation":"assertPublicUrl is an SSRF guard for server-side fetches: before fetching a caller-supplied URL, it rejects hostnames explicitly listed as internal (localhost, ip6-localhost, ip6-loopback). Line 52 throws when the URL's hostname (lowercased) is in BLOCKED_HOSTNAMES. The guard exists so an API caller cannot make the server request itself or loopback services.","triggerScenarios":"Calling resolveBaseUrl / POST / handleFetch with a URL whose hostname is exactly `localhost`, `ip6-localhost`, or `ip6-loopback` (e.g. `http://localhost:8080/api`).","commonSituations":"Developer points the fetch-proxy at a locally running service during development; config/env still contains `localhost` from a local setup moved to a server; testing the proxy endpoint with a loopback URL from an HTTP client.","solutions":["Point the URL at the public hostname of the target service instead of localhost","If the target really must be loopback, perform the fetch outside this guarded API","Use a fully-qualified public domain reachable from the server"],"exampleFix":"// before\nawait fetchViaProxy(\"http://localhost:8080/api\");\n// throws: Blocked URL: internal host\n\n// after\nawait fetchViaProxy(\"https://api.example.com/api\");","handlingStrategy":"validation","validationCode":"const BLOCKED = new Set([\"localhost\", \"ip6-localhost\", \"ip6-loopback\"]);\nfunction isInternalHost(rawUrl) {\n  try { return BLOCKED.has(new URL(rawUrl).hostname.toLowerCase()); }\n  catch { return true; }\n}\nif (isInternalHost(targetUrl)) console.warn(\"Refusing to send internal host through proxy\");","typeGuard":"function isPublicHttpUrl(v) {\n  if (typeof v !== \"string\") return false;\n  try {\n    const u = new URL(v);\n    return (u.protocol === \"http:\" || u.protocol === \"https:\") && !BLOCKED.has(u.hostname.toLowerCase());\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await proxyFetch(url);\n} catch (e) {\n  if (e.message === \"Blocked URL: internal host\") {\n    throw new HttpError(400, \"Target URL must be a public hostname, not localhost\");\n  }\n  throw e;\n}","preventionTips":["Never configure this server-side fetch proxy with localhost targets — it runs on the server, not your machine","Use environment-specific public hostnames in config rather than localhost","For local development, call local services directly instead of via the guarded endpoint","Add the hostname check to your own pre-flight validation before calling the proxy API"],"tags":["ssrf","security","network","url-validation"],"backgroundTag":"ssrf-blocked-internal-host","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}