{"record":{"id":"207f84d271ba4a4a","repo":"mastra-ai/mastra","slug":"push-notification-host-is-not-allowed-hostname","errorCode":null,"errorMessage":"Push notification host is not allowed: ${hostname}","messagePattern":"Push notification host is not allowed: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/server/src/server/a2a/push-notification-sender.ts","lineNumber":87,"sourceCode":"      lookup?: typeof defaultLookup;\n      allowedHosts?: string[];\n    } = {},\n  ) {}\n\n  getStore() {\n    return this.pushNotificationStore;\n  }\n\n  private async resolveValidatedDestination(rawUrl: string) {\n    const url = new URL(rawUrl);\n\n    if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n      throw new Error(`Push notification URL must use http or https: ${url.protocol}`);\n    }\n\n    const hostname = url.hostname.toLowerCase();\n    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    }","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/packages/server/src/server/a2a/push-notification-sender.ts#L69-L105","documentation":"When the sender is configured with an `allowedHosts` allowlist, any push notification destination whose lowercased hostname is not in that list is rejected. This is a host-based SSRF/egress control; the library throws instead of silently skipping so the config owner notices.","triggerScenarios":"A push notification config URL targets a hostname (after URL parsing and lowercasing) not present in `options.allowedHosts`, e.g. allowedHosts ['hooks.example.com'] but URL host is 'hooks.example.net' or 'hooks.example.com.' variants.","commonSituations":"Allowlist drift after infrastructure changes (new webhook domain), trailing-dot or case-normalization mismatches, subdomains not listed (allowlist is exact-match, not suffix match), or testing against localhost while the allowlist only has production hosts.","solutions":["Add the destination hostname (exact, lowercase) to the allowedHosts option of the push notification sender","Verify the URL hostname matches the allowlist entry exactly, including subdomains","Remove the allowlist (not recommended) if all hosts should be permitted","If the domain legitimately changed, update both the push config URL and the allowlist together"],"exampleFix":"// before\nnew PushNotificationSender({ allowedHosts: ['hooks.example.com'] })\n// url host: hooks.example.net -> rejected\n// after\nnew PushNotificationSender({ allowedHosts: ['hooks.example.com', 'hooks.example.net'] })","handlingStrategy":"validation","validationCode":"function assertAllowedHost(raw: string, allowedHosts: string[]) {\n  const host = new URL(raw).hostname.toLowerCase();\n  if (!allowedHosts.includes(host)) throw new Error(`Host ${host} not in allowlist`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await sender.sendNotifications(task, configs);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Push notification host is not allowed')) {\n    logger.error('Push host blocked by allowlist', { err: err.message });\n  } else throw err;\n}","preventionTips":["Keep allowedHosts in sync with your actual webhook domains","Remember the allowlist is exact-match, not suffix match — list subdomains explicitly","Compare hostnames lowercased, as the sender does"],"tags":["a2a","allowlist","ssrf","configuration"],"backgroundTag":"host-not-allowed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}