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
- Use a public https hostname that resolves to a public address as the callback
- For local testing, use an https tunnel to expose the local server under a public name
- 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
- Never use localhost, LAN IPs, Docker/K8s internal addresses, or CGNAT ranges as callbacks
- For local testing use an https tunnel with a public name
- Keep private hosts out of shared configuration templates
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
- callbackUrl hostname is a blocked metadata endpoint
- callbackUrl resolves to a private/reserved address
- callbackUrl is not allowed
- callbackUrl is not a valid URL
- callbackUrl is required
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/c79ea4f60298f1b3.
Report an issue: GitHub.