{"record":{"id":"6008509f84007017","repo":"mastra-ai/mastra","slug":"push-notification-url-resolved-to-a-local-or-priva","errorCode":null,"errorMessage":"Push notification URL resolved to a local or private IP: ${hostname}","messagePattern":"Push notification URL resolved to a local or private IP: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/server/src/server/a2a/push-notification-sender.ts","lineNumber":104,"sourceCode":"    if (this.options.allowedHosts && !this.options.allowedHosts.includes(hostname)) {\n      throw new Error(`Push notification host is not allowed: ${hostname}`);\n    }\n\n    if (isDisallowedHostname(hostname)) {\n      throw new Error(`Push notification URL must not target local or internal hosts: ${hostname}`);\n    }\n\n    if (isDisallowedIpAddress(hostname)) {\n      throw new Error(`Push notification URL must not target local or private IPs: ${hostname}`);\n    }\n\n    const resolvedAddresses =\n      isIP(hostname) === 0\n        ? await (this.options.lookup ?? defaultLookup)(hostname, { all: true, verbatim: true })\n        : [{ address: hostname, family: isIP(hostname) }];\n\n    if (resolvedAddresses.some(result => isDisallowedIpAddress(result.address))) {\n      throw new Error(`Push notification URL resolved to a local or private IP: ${hostname}`);\n    }\n\n    const requestUrl = new URL(url.toString());\n    requestUrl.hostname = resolvedAddresses[0]!.address;\n\n    return {\n      originalUrl: url,\n      requestUrl,\n      hostHeader: url.host,\n      servername: isIP(hostname) === 0 ? hostname : undefined,\n    };\n  }\n\n  private async postTaskSnapshot({\n    requestUrl,\n    hostHeader,\n    servername,\n    headers,","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/a2a/push-notification-sender.ts#L86-L122","documentation":"To defeat DNS-rebinding, the sender resolves the hostname itself (DNS lookup with all addresses) and rejects it if any resolved address is local/private, throwing this error before pinning requestUrl.hostname to the first resolved address. The request is never made against an unvalidated IP.","triggerScenarios":"A hostname-backed push URL whose DNS currently resolves to a private/loopback/link-local address — e.g. a DNS record pointing at 127.0.0.1, a split-horizon DNS returning 10.x inside the cluster, or a tunneled domain resolving to a CGNAT/private range.","commonSituations":"DNS-rebinding attempts (attacker-controlled domain flipping to 169.254.169.254), corporate DNS resolving public names to internal IPs, split-horizon setups where the server sees internal answers, or recently changed DNS records pointing at private targets.","solutions":["Fix the DNS record so the hostname resolves to a public IP","Bypass split-horizon DNS by using a publicly resolvable name for the callback","Check resolution with `dig`/`nslookup` from the server host to see what addresses come back","Provide a custom `options.lookup` resolver if your environment requires specific DNS behavior (must still return public IPs)"],"exampleFix":"// before\n$ dig push.example.com -> 10.0.0.7 (internal A record)\n// after\n$ dig push.example.com -> 203.0.113.7 (public A record)","handlingStrategy":"validation","validationCode":"import { lookup } from 'node:dns/promises';\nasync function assertResolvesPublic(raw: string) {\n  const host = new URL(raw).hostname;\n  const addrs = await lookup(host, { all: true, verbatim: true });\n  if (addrs.length === 0) throw new Error(`No DNS records for ${host}`);\n  // additionally assert none are private before registering the push config\n}","typeGuard":null,"tryCatchPattern":"try {\n  await sender.sendNotifications(task, configs);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Push notification URL resolved to a local or private IP')) {\n    logger.error('DNS rebinding/split-horizon suspected', { err: err.message });\n  } else throw err;\n}","preventionTips":["Check DNS resolution from the server host, not your laptop (split-horizon differences)","Ensure all A/AAAA records for the webhook domain are public","Keep DNS TTLs sane and re-validate records after infrastructure changes"],"tags":["a2a","ssrf","dns","dns-rebinding"],"backgroundTag":"dns-resolves-to-private-ip","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}