{"record":{"id":"ae3665b5f36f3f19","repo":"koala73/worldmonitor","slug":"webhook-url-must-not-point-to-a-metadata-endpoint","errorCode":null,"errorMessage":"Webhook URL must not point to a metadata endpoint","messagePattern":"Webhook URL must not point to a metadata endpoint","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"error","filePath":"api/_notification-webhook-ssrf.ts","lineNumber":223,"sourceCode":"async function defaultResolveHostname(hostname: string): Promise<string[]> {\n  const records = await Promise.all([\n    resolveDnsJson(hostname, 'A'),\n    resolveDnsJson(hostname, 'AAAA'),\n  ]);\n  return records.flat();\n}\n\n/**\n * Fail fast at registration when the webhook hostname currently resolves to a\n * private or reserved address. Delivery repeats this check (and pins its\n * connection) because DNS can change after registration.\n */\nexport async function assertNotificationWebhookRegistrationUrlSafe(\n  rawUrl: string,\n  resolveHostname: ResolveHostname = defaultResolveHostname,\n): Promise<void> {\n  const staticError = blockedNotificationWebhookUrlReason(rawUrl);\n  if (staticError) throw new Error(staticError);\n\n  const hostname = new URL(rawUrl).hostname.toLowerCase();\n  if (isIpLiteral(hostname)) return;\n  let resolvedAddresses: string[];\n  try {\n    resolvedAddresses = await resolveHostname(hostname);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    throw new Error(`Webhook URL DNS resolution failed: ${message}`);\n  }\n  if (!resolvedAddresses.length) throw new Error('Webhook URL DNS resolution returned no addresses');\n  if (resolvedAddresses.some(isBlockedNotificationResolvedAddress)) {\n    throw new Error('Webhook URL must not point to a private/local address');\n  }\n}\n","sourceCodeStart":205,"sourceCodeEnd":239,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/_notification-webhook-ssrf.ts#L205-L239","documentation":"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.","triggerScenarios":"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/`.","commonSituations":"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.","solutions":["Point the webhook at a real, public, https endpoint that you control.","Remove any localhost/metadata hostnames from your test fixtures and configuration.","Treat the rejection as expected behavior — do not attempt to bypass it; the block is intentional defense-in-depth."],"exampleFix":"// before\nregisterWebhook('https://169.254.169.254/latest/meta-data/')\n// after\nregisterWebhook('https://your-actual-service.example.com/webhook')","handlingStrategy":"validation","validationCode":"const BLOCKED_METADATA_HOSTNAMES = new Set([\n  'localhost','169.254.169.254','metadata.google.internal','metadata.internal',\n  'instance-data','metadata','computemetadata','link-local.s3.amazonaws.com',\n]);\n\nfunction isMetadataHost(rawUrl: string): boolean {\n  try { return BLOCKED_METADATA_HOSTNAMES.has(new URL(rawUrl).hostname.toLowerCase()); }\n  catch { return false; }\n}\n\nif (isMetadataHost(input)) return res.status(400).json({ error: 'Metadata/localhost hosts are not permitted.' });","typeGuard":"function isBlockedMetadataHostname(value: unknown): boolean {\n  if (typeof value !== 'string') return false;\n  try { return BLOCKED_METADATA_HOSTNAMES.has(new URL(value).hostname.toLowerCase()); } catch { return false; }\n}","tryCatchPattern":"try {\n  await assertNotificationWebhookRegistrationUrlSafe(rawUrl);\n} catch (err) {\n  if (err.message === 'Webhook URL must not point to a metadata endpoint') {\n    return res.status(400).json({ error: 'Cloud-metadata and localhost hosts are blocked.' });\n  }\n  throw err;\n}","preventionTips":["Maintain the BLOCKED_METADATA_HOSTNAMES list alongside any new cloud-provider metadata endpoint your infra uses.","Treat rejections of metadata hosts as expected behavior in security tests; do not file bug reports for them.","Keep webhook test fixtures pointed at public test sinks (webhook.site, requestbin) over https."],"tags":["webhook","ssrf","security","metadata","validation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}