koala73/worldmonitor · error · Error

Webhook URL must not point to a metadata endpoint

Error message

Webhook URL must not point to a metadata endpoint

What it means

Returned when the webhook URL's hostname matches BLOCKED_METADATA_HOSTNAMES (localhost, 169.254.169.254, metadata.google.internal, metadata.internal, instance-data, metadata, computemetadata, link-local.s3.amazonaws.com). This is the cloud-metadata SSRF guard: it stops a webhook from exfiltrating instance credentials from AWS/GCP/Azure IMDS or similar metadata services.

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. Point the webhook at a real, public, https endpoint that you control.
  2. Remove any localhost/metadata hostnames from your test fixtures and configuration.
  3. Treat the rejection as expected behavior — do not attempt to bypass it; the block is intentional defense-in-depth.

Example fix

// before
registerWebhook('https://169.254.169.254/latest/meta-data/')
// after
registerWebhook('https://your-actual-service.example.com/webhook')
Defensive patterns

Strategy: validation

Validate before calling

const BLOCKED_METADATA_HOSTNAMES = new Set([
  'localhost','169.254.169.254','metadata.google.internal','metadata.internal',
  'instance-data','metadata','computemetadata','link-local.s3.amazonaws.com',
]);

function isMetadataHost(rawUrl: string): boolean {
  try { return BLOCKED_METADATA_HOSTNAMES.has(new URL(rawUrl).hostname.toLowerCase()); }
  catch { return false; }
}

if (isMetadataHost(input)) return res.status(400).json({ error: 'Metadata/localhost hosts are not permitted.' });

Type guard

function isBlockedMetadataHostname(value: unknown): boolean {
  if (typeof value !== 'string') return false;
  try { return BLOCKED_METADATA_HOSTNAMES.has(new URL(value).hostname.toLowerCase()); } catch { return false; }
}

Try / catch

try {
  await assertNotificationWebhookRegistrationUrlSafe(rawUrl);
} catch (err) {
  if (err.message === 'Webhook URL must not point to a metadata endpoint') {
    return res.status(400).json({ error: 'Cloud-metadata and localhost hosts are blocked.' });
  }
  throw err;
}

Prevention

When it happens

Trigger: Webhook registration with a URL whose hostname is literally one of the blocked metadata endpoints, e.g. `https://169.254.169.254/latest/meta-data/` or `https://metadata.google.internal/computeMetadata/`.

Common situations: A security researcher probing the webhook feature for SSRF; an accidental localhost URL left in a test fixture; a misconfigured integration that was pointed at a metadata-like hostname.

Related errors


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