koala73/worldmonitor · warning · Error

callbackUrl resolves to a private/reserved address: ${hostna

Error message

callbackUrl resolves to a private/reserved address: ${hostname}

What it means

Static SSRF-policy check in isBlockedCallbackUrl (webhook-shared.ts:71-77, rethrown at :121): the hostname itself is an IP literal flagged by isBlockedResolvedAddress, or matches PRIVATE_HOSTNAME_PATTERNS — localhost, 127/8, 10/8, 192.168/16, 172.16-31, 169.254/16, IPv6 ULA fd00::/8, link-local fe80:, ::1, 0.0.0.0, 0/8, and CGNAT 100.64/10. The offending hostname is echoed in the message. (The separate post-DNS message at :134 omits the hostname.)

Source

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

    return (data.Answer ?? [])
      .filter(answer => answer.type === expectedType && typeof answer.data === 'string')
      .map(answer => answer.data!);
  };
  const records = await Promise.all([resolveRecordType('A'), resolveRecordType('AAAA')]);
  return records.flat();
}

/**
 * Validate the current DNS answer before storing a webhook. Delivery makes the
 * same check immediately before send and pins the resulting socket, which
 * keeps this fail-fast check from becoming the only SSRF control.
 */
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);

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Use a public https hostname that resolves to a public address as the callback
  2. For local testing, use an https tunnel to expose the local server under a public name
  3. Strip private/literal hosts from configuration templates before they reach RegisterWebhook

Example fix

// before
registerWebhook(ctx, { callbackUrl: 'https://10.1.2.3/cb', chokepointIds });
// after
registerWebhook(ctx, { callbackUrl: 'https://hooks.example.com/cb', chokepointIds });
Defensive patterns

Strategy: validation

Validate before calling

const PRIVATE = [/^localhost$/i, /^127\./, /^10\./, /^192\.168\./, /^172\.(1[6-9]|2\d|3[01])\./, /^169\.254\./, /^fd[0-9a-f]{2}:/i, /^fe80:/i, /^::1$/, /^0\./, /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./];
const h = new URL(callbackUrl).hostname.toLowerCase();
if (PRIVATE.some(p => p.test(h))) throw new RangeError('private/reserved callback hosts are not allowed');

Type guard

const isPublicCallbackHost = (v: unknown): v is string => { if (typeof v !== 'string') return false; try { const h = new URL(v).hostname.toLowerCase(); return !PRIVATE_PATTERNS.some(p => p.test(h)) && !isPrivateIpLiteral(h); } catch { return false; } };

Try / catch

catch (e) { if (/private\/reserved address/.test(e?.details?.[0]?.description ?? '')) { move the receiver to a public https hostname and re-submit } else throw e; }

Prevention

When it happens

Trigger: POST RegisterWebhook whose callbackUrl host is a private/reserved IP literal (https://10.0.0.5/cb, https://[fd12::1]/cb) or the literal name 'localhost'. Because isIpLiteral() short-circuits at :124, IP-literal hosts are judged by this static check alone — no DNS resolution happens for them.

Common situations: Local development pointing at http://localhost:3000 or a LAN IP; Docker/Kubernetes internal addresses used as callbacks; staging configs with intranet IPs; quick smoke tests against 127.0.0.1.

Related errors


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