{"record":{"id":"cb016d469796f57b","repo":"koala73/worldmonitor","slug":"callbackurl-resolves-to-a-private-reserved-address-cb016d","errorCode":null,"errorMessage":"callbackUrl resolves to a private/reserved address","messagePattern":"callbackUrl resolves to a private/reserved address","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"warning","filePath":"server/worldmonitor/shipping/v2/webhook-shared.ts","lineNumber":134,"sourceCode":"export async function assertCallbackUrlRegistrationSafe(\n  callbackUrl: string,\n  resolveHostname: ResolveHostname = defaultResolveHostname,\n): Promise<void> {\n  const staticError = isBlockedCallbackUrl(callbackUrl);\n  if (staticError) throw new Error(staticError);\n\n  const hostname = new URL(callbackUrl).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(`callbackUrl DNS resolution failed: ${message}`);\n  }\n  if (!resolvedAddresses.length) throw new Error('callbackUrl DNS resolution returned no addresses');\n  const blocked = resolvedAddresses.find(isBlockedResolvedAddress);\n  if (blocked) throw new Error('callbackUrl resolves to a private/reserved address');\n}\n\nexport async function generateSecret(): Promise<string> {\n  const bytes = new Uint8Array(32);\n  crypto.getRandomValues(bytes);\n  return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\nexport function generateSubscriberId(): string {\n  const bytes = new Uint8Array(12);\n  crypto.getRandomValues(bytes);\n  return 'wh_' + [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');\n}\n\nexport function webhookKey(subscriberId: string): string {\n  return `webhook:sub:${subscriberId}:v1`;\n}\n","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/worldmonitor/shipping/v2/webhook-shared.ts#L116-L152","documentation":"Post-resolution SSRF check in assertCallbackUrlRegistrationSafe: after DNS resolves the callback hostname, every returned address is tested with isBlockedResolvedAddress (from server/_shared/ip-address-classification, covering RFC1918, loopback, link-local, CGNAT, reserved, and IPv6 ULA/link-local ranges). If any address is blocked, registration rejects. Unlike the static literal check at :121, this catches names that are not literals but resolve to private space — including DNS-rebinding setups, which is why delivery re-runs the same check before send.","triggerScenarios":"POST RegisterWebhook with a normal-looking hostname whose public DNS answer includes a private/reserved IP: split-horizon DNS (name resolves internally to 10.x), a domain whose A record actually points at 192.168.x.x or 169.254.169.254, or a rebinding setup where the record flips between public and private.","commonSituations":"Corporate domains that resolve to internal IPs from some vantage points; misconfigured public DNS zones containing private addresses; intentional SSRF/rebinding attempts against the delivery worker; testing with hosts that point at home/LAN networks.","solutions":["Point the callback at a hostname whose public A/AAA records are all public addresses","Fix the DNS zone: remove private-address records from the public zone or publish correct public records","If the target legitimately lives in private space, it cannot be a WorldMonitor webhook target — expose it via a public endpoint"],"exampleFix":"// before\n// dns: hooks.example.com -> 10.0.0.9 (split-horizon leak)\nawait registerWebhook({ callbackUrl: 'https://hooks.example.com/cb', chokepointIds });\n// after\n// dns: hooks.example.com -> 203.0.113.9 (public)\nawait registerWebhook({ callbackUrl: 'https://hooks.example.com/cb', chokepointIds });","handlingStrategy":"validation","validationCode":"// resolve the host and assert every address is public before registering\nconst addrs = await resolveAll(host); // A + AAAA\nif (addrs.length === 0 || addrs.some(isPrivateOrReserved)) throw new RangeError('callback host resolves to a private/reserved address');","typeGuard":null,"tryCatchPattern":"catch (e) { if (e?.details?.[0]?.description === 'callbackUrl resolves to a private/reserved address') { fix the public DNS zone to serve public addresses, then re-submit } else throw e; }","preventionTips":["Check that public DNS serves public addresses for the callback host from every vantage point (no split-horizon leaks)","Remember the same check re-runs before each delivery — a later rebinding to private space will start failing sends","Never point callbacks at home/LAN or intranet addresses"],"tags":["ssrf","security","dns","private-ip","webhooks","http-400","rebinding"],"backgroundTag":"ssrf-protection","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}