{"record":{"id":"18655adb282cb999","repo":"koala73/worldmonitor","slug":"webhook-url-must-not-point-to-a-private-local-addr","errorCode":null,"errorMessage":"Webhook URL must not point to a private/local address","messagePattern":"Webhook URL must not point to a private/local address","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"api/_notification-webhook-ssrf.ts","lineNumber":223,"sourceCode":"async function defaultResolveHostname(hostname: string): Promise<string[]> {\n  const records = await Promise.all([\n    resolveDnsJson(hostname, 'A'),\n    resolveDnsJson(hostname, 'AAAA'),\n  ]);\n  return records.flat();\n}\n\n/**\n * Fail fast at registration when the webhook hostname currently resolves to a\n * private or reserved address. Delivery repeats this check (and pins its\n * connection) because DNS can change after registration.\n */\nexport async function assertNotificationWebhookRegistrationUrlSafe(\n  rawUrl: string,\n  resolveHostname: ResolveHostname = defaultResolveHostname,\n): Promise<void> {\n  const staticError = blockedNotificationWebhookUrlReason(rawUrl);\n  if (staticError) throw new Error(staticError);\n\n  const hostname = new URL(rawUrl).hostname.toLowerCase();\n  if (isIpLiteral(hostname)) return;\n  let resolvedAddresses: string[];\n  try {\n    resolvedAddresses = await resolveHostname(hostname);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    throw new Error(`Webhook URL DNS resolution failed: ${message}`);\n  }\n  if (!resolvedAddresses.length) throw new Error('Webhook URL DNS resolution returned no addresses');\n  if (resolvedAddresses.some(isBlockedNotificationResolvedAddress)) {\n    throw new Error('Webhook URL must not point to a private/local address');\n  }\n}\n","sourceCodeStart":205,"sourceCodeEnd":239,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/_notification-webhook-ssrf.ts#L205-L239","documentation":"Returned by blockedNotificationWebhookUrlReason when the hostname is an IP literal (or otherwise directly classified) that isBlockedNotificationResolvedAddress flags as private/reserved — loopback, RFC1918 ranges, link-local, CGNAT 100.64/10, ULA, multicast, documentation ranges, etc. This is the static SSRF gate that catches an attacker (or misconfiguration) pointing the webhook directly at an internal IP before any DNS resolution is attempted.","triggerScenarios":"Webhook registration with a URL whose host is a private IP literal such as `https://10.0.0.5/`, `https://127.0.0.1/`, `https://192.168.1.1/`, `https://[::1]/`, or any RFC1918/loopback/link-local/ULA address.","commonSituations":"Internal-tool webhook mistakenly registered against a private network address; adversarial SSRF probe; a stale config pointing at a decommissioned internal service.","solutions":["Register an https URL whose host resolves to a public IP you control.","If the webhook legitimately lives behind a VPN/private network, expose it via a public ingress (load balancer, API gateway, tunnel) before registering.","Re-run the registration through the public hostname only."],"exampleFix":"// before\nregisterWebhook('https://10.0.0.5/internal-hook')\n// after\nregisterWebhook('https://ingress.example.com/internal-hook')","handlingStrategy":"validation","validationCode":"import { isBlockedNotificationResolvedAddress } from './_notification-webhook-ssrf';\n\nfunction webhookHostIsBlockedIpLiteral(rawUrl: string): boolean {\n  try {\n    const host = new URL(rawUrl).hostname.toLowerCase();\n    return isBlockedNotificationResolvedAddress(host);\n  } catch { return false; }\n}\n\nif (webhookHostIsBlockedIpLiteral(input)) {\n  return res.status(400).json({ error: 'Webhook URL must not point to a private/local address.' });\n}","typeGuard":"import { isBlockedNotificationResolvedAddress } from './_notification-webhook-ssrf';\n\nfunction isPublicWebhookHost(value: unknown): boolean {\n  if (typeof value !== 'string') return false;\n  try {\n    const host = new URL(value).hostname.toLowerCase();\n    return !isBlockedNotificationResolvedAddress(host);\n  } catch { return false; }\n}","tryCatchPattern":"try {\n  await assertNotificationWebhookRegistrationUrlSafe(rawUrl);\n} catch (err) {\n  if (err.message === 'Webhook URL must not point to a private/local address') {\n    return res.status(400).json({ error: 'Point the webhook at a public https endpoint.' });\n  }\n  throw err;\n}","preventionTips":["Always expose webhooks via a public ingress; never register private-network addresses.","When self-hosting, put the webhook behind a load balancer/API gateway with a public IP.","Educate users that private-IP webhooks are blocked by design (SSRF defense)."],"tags":["webhook","ssrf","security","private-ip","validation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}