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
- 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
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
- 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
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
- callbackUrl resolves to a private/reserved address: ${hostna
- callbackUrl hostname is a blocked metadata endpoint
- callbackUrl is not allowed
- DNS ${recordType} lookup failed: HTTP ${response.status}
- callbackUrl is not a valid URL
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/cb016d469796f57b.
Report an issue: GitHub.