koala73/worldmonitor · warning · Error

callbackUrl hostname is a blocked metadata endpoint

Error message

callbackUrl hostname is a blocked metadata endpoint

What it means

Static SSRF-policy check in isBlockedCallbackUrl: if the URL's lowercase hostname is in BLOCKED_METADATA_HOSTNAMES (169.254.169.254, metadata.google.internal, metadata.internal, instance-data, metadata, computemetadata, link-local.s3.amazonaws.com), registration rejects with this message (thrown at webhook-shared.ts:121, surfaced as 400 by registerWebhook). These hostnames expose cloud instance metadata (credentials, IAM tokens) and are the primary SSRF jackpot.

Solutions

  1. Point the callback at your own public https endpoint — metadata hostnames can never be webhook targets
  2. If this appeared in logs unexpectedly, treat it as a potential SSRF attempt and review the caller/credentials that submitted it
  3. Audit any integration templates or scripts that auto-generate callbackUrl from environment discovery

Example fix

// before
registerWebhook(ctx, { callbackUrl: 'http://metadata.google.internal/computeMetadata/v1', chokepointIds });
// after
registerWebhook(ctx, { callbackUrl: 'https://status.example.com/webhooks/worldmonitor', chokepointIds });
Defensive patterns

Strategy: validation

Validate before calling

const BLOCKED = new Set(['169.254.169.254','metadata.google.internal','metadata.internal','instance-data','metadata','computemetadata','link-local.s3.amazonaws.com']);
if (BLOCKED.has(new URL(callbackUrl).hostname.toLowerCase())) throw new RangeError('metadata endpoints cannot be webhook targets');

Try / catch

catch (e) { if (e?.details?.[0]?.description?.includes('blocked metadata endpoint')) { replace the callback with your own public https endpoint } else throw e; }

Prevention

When it happens

Trigger: POST RegisterWebhook with a callbackUrl pointing at a cloud metadata service, e.g. https://169.254.169.254/latest/meta-data or http://metadata.google.internal/computeMetadata/v1 — either as an attack or as a misconfigured internal integration.

Common situations: Security testing/probing of the webhook feature; a partner accidentally pasting an internal cloud URL; automated tooling that discovers and registers internal endpoints; attempts to make WorldMonitor delivery fetch instance credentials.

Related errors


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

Appendix: 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)