{"record":{"id":"9966f235f26c6674","repo":"amir20/dozzle","slug":"webhook-target-resolves-to-a-blocked-address-range","errorCode":null,"errorMessage":"webhook target resolves to a blocked address range","messagePattern":"webhook target resolves to a blocked address range","errorType":"validation","errorClass":"errBlockedAddress","httpStatus":null,"severity":"warning","filePath":"internal/notification/dispatcher/webhook.go","lineNumber":28,"sourceCode":"\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\t\"text/template\"\n\t\"time\"\n\n\t\"github.com/amir20/dozzle/types\"\n\t\"github.com/rs/zerolog/log\"\n)\n\n// errBlockedAddress is returned when a webhook URL resolves to a blocked\n// address range. Loopback and link-local addresses are refused to prevent SSRF\n// against the Dozzle host's own services and cloud metadata endpoints\n// (e.g. 169.254.169.254). RFC1918 private ranges are intentionally allowed —\n// self-hosted webhooks (Home Assistant, internal Mattermost, etc.) commonly\n// live on private LANs.\nvar errBlockedAddress = errors.New(\"webhook target resolves to a blocked address range\")\n\n// zeroNetV4 covers 0.0.0.0/8 — on Linux these route to the local host.\nvar zeroNetV4 = &net.IPNet{IP: net.IP{0, 0, 0, 0}, Mask: net.CIDRMask(8, 32)}\n\nfunc isBlockedIP(ip net.IP) bool {\n\tif isBlockedBaseIP(ip) {\n\t\treturn true\n\t}\n\t// IPv6 transition mechanisms (6to4, NAT64, Teredo, IPv4-compatible) embed an\n\t// arbitrary IPv4 address that none of the checks above look at. Unwrap and\n\t// re-check the embedded address so 2002:7f00:1::1 is treated as 127.0.0.1.\n\treturn slices.ContainsFunc(embeddedIPv4(ip), isBlockedBaseIP)\n}\n\nfunc isBlockedBaseIP(ip net.IP) bool {\n\tif ip.IsLoopback() ||\n\t\tip.IsLinkLocalUnicast() ||\n\t\tip.IsLinkLocalMulticast() ||","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/amir20/dozzle/blob/d9463cbe21874e44ab79db6fa63e746ca7d22928/internal/notification/dispatcher/webhook.go#L10-L46","documentation":"errBlockedAddress is returned when a webhook URL's DNS resolution points into a blocked IP range (loopback, link-local, 0.0.0.0/8, etc.). Dozzle refuses these to prevent SSRF against its own host services and cloud metadata endpoints (169.254.169.254), while deliberately allowing RFC1918 private LANs for self-hosted webhooks.","triggerScenarios":"safeDialContext inspects resolved IPs during dialing and isBlockedIP matches (webhook.go:132); SendTest propagates it. Any webhook send to a URL whose hostname resolves to loopback, link-local, or 0.0.0.0/8 triggers it; when every resolved IP is blocked, lastErr falls back to errBlockedAddress (webhook.go:142).","commonSituations":"Pointing the webhook at localhost/127.0.0.1 or the host's own metadata service (intentionally or via config mistake); a DNS name that resolves to 169.254.x.x; hostname like 'localhost' or 0.0.0.0 in the destination URL; DNS rebinding-ish setups where internal names resolve to loopback.","solutions":["Change the webhook URL to point at a routable address (a real LAN IP in RFC1918 space is allowed, e.g. 192.168.x.x, 10.x.x.x, 172.16-31.x.x).","If the target genuinely is a local service, expose it on a private/LAN interface and use that address instead of 127.0.0.1/localhost.","Check DNS: dig <host> and confirm none of the resolved IPs are loopback/link-local/0.0.0.0/8.","If it was a misconfigured or malicious destination, remove or correct the destination in the notification settings."],"exampleFix":"// before\nurl: http://127.0.0.1:8123/api/webhook\n// after\nurl: http://192.168.1.50:8123/api/webhook","handlingStrategy":"validation","validationCode":"// resolve the webhook host before saving the destination\nips, err := net.LookupIP(host)\nif err != nil || len(ips) == 0 { return fmt.Errorf(\"cannot resolve webhook host\") }\nfor _, ip := range ips {\n  if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {\n    return fmt.Errorf(\"webhook host %s resolves to blocked address %s\", host, ip)\n  }\n}","typeGuard":null,"tryCatchPattern":"resp, err := client.SendTest(dest)\nif errors.Is(err, errBlockedAddress) {\n  // surface 'destination resolves to a blocked address range' to the user\n}","preventionTips":["Never point webhooks at 127.0.0.1, localhost, 0.0.0.0, or 169.254.x.x","Use RFC1918 LAN addresses for self-hosted webhook targets (those are allowed)","Resolve the hostname and inspect IPs before saving a destination","Be aware the block is SSRF protection; it will not be relaxed for loopback"],"tags":["ssrf","security","webhook","network"],"backgroundTag":"path-traversal-blocked","analyzedSha":"d9463cbe21874e44ab79db6fa63e746ca7d22928","analyzedAt":"2026-09-07T10:08:55.855Z","contentChangedAt":"2026-09-07T10:08:55.855Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}