GHSA-p2w3-6x73-2f6x: SSRF
Summary
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:
go 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
1. Authenticated user creates a webhook notification with URL http://[2002:7f00:0001::1]:8080/ (6to4 embedding 127.0.0.1) 2. When a notification triggers, Dozzle's webhook dispatcher calls safeDialContext 3. The IPv6 address 2002:7f00:0001::1 is checked against isBlockedIP -- all predicates return false 4. Connection proceeds to the 6to4 relay which routes to 127.0.0.1 5. The webhook POST reaches the host's loopback services
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) or 2002: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.
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:
go 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
Remediation
Add IPv6 transition mechanism prefix checks to isBlockedIP:
go 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 }
Credit
Reported by tonghuaroot (tonghuaroot@gmail.com).
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/amir20/dozzleto a version that resolves this vulnerability.Fixed in 1.29.1-0.20260804193351-8cf7ccd5ee04 - Configuration
Modify `isBlockedIP` in `internal/notification/dispatcher/webhook.go` to recognize IPv6 transition mechanism addresses (6to4/NAT64/Teredo) that embed arbitrary IPv4, and block them consistent with the existing blocked goals (loopback, link-local/169.254.0.0/16, etc.).
Dozzle internal/notification/dispatcher/webhook.go (SSRF guard isBlockedIP) isBlockedIP IPv6 transition mechanism prefix checks = Add explicit checks for IPv6 transition mechanism prefixes (RFC 3056 6to4, RFC 6052 NAT64, RFC 4380 Teredo) so they are treated as blocked when they embed loopback and/or 169.254.169.254
Event History
Frequently Asked Questions
Who can exploit this issue?
An authenticated user who can configure or trigger webhook notification URLs can exploit it. The vulnerable SSRF guard is in Dozzle's webhook notification dispatcher.
What internal targets could an attacker reach?
By using IPv6 transition-mechanism addresses that embed IPv4 addresses, an attacker can bypass the guard to access loopback services, cloud metadata endpoints such as 169.254.169.254, and other ranges the guard is intended to block.
Are all Dozzle deployments affected by default?
The issue affects all versions that include the SSRF guard, including the cited current HEAD commit b9df313. Exploitation additionally requires an authenticated user to be able to use webhook notification URLs.
How can I determine whether my deployment is exposed?
Check whether your Dozzle version includes the webhook dispatcher SSRF guard in internal/notification/dispatcher/webhook.go and whether authenticated users can configure or cause delivery to webhook URLs. Deployments without usable webhook notification URLs for authenticated users do not meet the stated exploitation condition.