koala73/worldmonitor · error · Error

Webhook URL must not point to a private/local address

Error message

Webhook URL must not point to a private/local address

What it means

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.

Source

Thrown at api/_notification-webhook-ssrf.ts:223

async function defaultResolveHostname(hostname: string): Promise<string[]> {
  const records = await Promise.all([
    resolveDnsJson(hostname, 'A'),
    resolveDnsJson(hostname, 'AAAA'),
  ]);
  return records.flat();
}

/**
 * Fail fast at registration when the webhook hostname currently resolves to a
 * private or reserved address. Delivery repeats this check (and pins its
 * connection) because DNS can change after registration.
 */
export async function assertNotificationWebhookRegistrationUrlSafe(
  rawUrl: string,
  resolveHostname: ResolveHostname = defaultResolveHostname,
): Promise<void> {
  const staticError = blockedNotificationWebhookUrlReason(rawUrl);
  if (staticError) throw new Error(staticError);

  const hostname = new URL(rawUrl).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(`Webhook URL DNS resolution failed: ${message}`);
  }
  if (!resolvedAddresses.length) throw new Error('Webhook URL DNS resolution returned no addresses');
  if (resolvedAddresses.some(isBlockedNotificationResolvedAddress)) {
    throw new Error('Webhook URL must not point to a private/local address');
  }
}

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Register an https URL whose host resolves to a public IP you control.
  2. If the webhook legitimately lives behind a VPN/private network, expose it via a public ingress (load balancer, API gateway, tunnel) before registering.
  3. Re-run the registration through the public hostname only.

Example fix

// before
registerWebhook('https://10.0.0.5/internal-hook')
// after
registerWebhook('https://ingress.example.com/internal-hook')
Defensive patterns

Strategy: validation

Validate before calling

import { isBlockedNotificationResolvedAddress } from './_notification-webhook-ssrf';

function webhookHostIsBlockedIpLiteral(rawUrl: string): boolean {
  try {
    const host = new URL(rawUrl).hostname.toLowerCase();
    return isBlockedNotificationResolvedAddress(host);
  } catch { return false; }
}

if (webhookHostIsBlockedIpLiteral(input)) {
  return res.status(400).json({ error: 'Webhook URL must not point to a private/local address.' });
}

Type guard

import { isBlockedNotificationResolvedAddress } from './_notification-webhook-ssrf';

function isPublicWebhookHost(value: unknown): boolean {
  if (typeof value !== 'string') return false;
  try {
    const host = new URL(value).hostname.toLowerCase();
    return !isBlockedNotificationResolvedAddress(host);
  } catch { return false; }
}

Try / catch

try {
  await assertNotificationWebhookRegistrationUrlSafe(rawUrl);
} catch (err) {
  if (err.message === 'Webhook URL must not point to a private/local address') {
    return res.status(400).json({ error: 'Point the webhook at a public https endpoint.' });
  }
  throw err;
}

Prevention

When it happens

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

Common situations: Internal-tool webhook mistakenly registered against a private network address; adversarial SSRF probe; a stale config pointing at a decommissioned internal service.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/18655adb282cb999. Report an issue: GitHub.