koala73/worldmonitor · warning · Error

callbackUrl resolves to a private/reserved address

Error message

callbackUrl resolves to a private/reserved address

What it means

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.

Source

Thrown at server/worldmonitor/shipping/v2/webhook-shared.ts:134

export async function assertCallbackUrlRegistrationSafe(
  callbackUrl: string,
  resolveHostname: ResolveHostname = defaultResolveHostname,
): Promise<void> {
  const staticError = isBlockedCallbackUrl(callbackUrl);
  if (staticError) throw new Error(staticError);

  const hostname = new URL(callbackUrl).hostname.toLowerCase();
  if (isIpLiteral(hostname)) return;
  let resolvedAddresses: string[];
  try {
    resolvedAddresses = await resolveHostname(hostname);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`callbackUrl DNS resolution failed: ${message}`);
  }
  if (!resolvedAddresses.length) throw new Error('callbackUrl DNS resolution returned no addresses');
  const blocked = resolvedAddresses.find(isBlockedResolvedAddress);
  if (blocked) throw new Error('callbackUrl resolves to a private/reserved address');
}

export async function generateSecret(): Promise<string> {
  const bytes = new Uint8Array(32);
  crypto.getRandomValues(bytes);
  return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
}

export function generateSubscriberId(): string {
  const bytes = new Uint8Array(12);
  crypto.getRandomValues(bytes);
  return 'wh_' + [...bytes].map(b => b.toString(16).padStart(2, '0')).join('');
}

export function webhookKey(subscriberId: string): string {
  return `webhook:sub:${subscriberId}:v1`;
}

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Point the callback at a hostname whose public A/AAA records are all public addresses
  2. Fix the DNS zone: remove private-address records from the public zone or publish correct public records
  3. If the target legitimately lives in private space, it cannot be a WorldMonitor webhook target — expose it via a public endpoint

Example fix

// before
// dns: hooks.example.com -> 10.0.0.9 (split-horizon leak)
await registerWebhook({ callbackUrl: 'https://hooks.example.com/cb', chokepointIds });
// after
// dns: hooks.example.com -> 203.0.113.9 (public)
await registerWebhook({ callbackUrl: 'https://hooks.example.com/cb', chokepointIds });
Defensive patterns

Strategy: validation

Validate before calling

// resolve the host and assert every address is public before registering
const addrs = await resolveAll(host); // A + AAAA
if (addrs.length === 0 || addrs.some(isPrivateOrReserved)) throw new RangeError('callback host resolves to a private/reserved address');

Try / catch

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; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/cb016d469796f57b. Report an issue: GitHub.