Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹 Go
Not in CISA KEV

CVE-2026-73087 — dozzle

Fix: amir20/dozzle@8cf7ccd

CVE-2026-73087 is a Server-Side Request Forgery (SSRF) vulnerability in github.com/amir20/dozzle. A fix is available for github.com/amir20/dozzle — see the affected versions and patch details below.

Dozzle: SSRF guard bypass via IPv6 transition addresses (6to4/NAT64/Teredo) in webhook notification dispatcher

Also known asGHSA-p2w3-6x73-2f6xGO-2026-6440
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 24, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-73087.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs39th percentile — riskier than 39% of all scored CVEsHighest risk
0.00%0.32%0.64%0.96%0.3%0.5%0.5%Sep 26Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

Real-World Exposure

1 pkg affected
🐹github.com/amir20/dozzle

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

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:

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:

MechanismPrefixEmbedsisBlockedIP result
6to42002::/16any IPv4 in bits 16-47false
NAT64 WKP64:ff9b::/96any IPv4 in bits 96-127false
Teredo2001:0000::/32any IPv4 in bits 96-127false

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:

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:

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 ([email protected]).

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/amir20/dozzleall versions1.29.1-0.20260804193351-8cf7ccd5ee04go get github.com/amir20/dozzle@v1.29.1-0.20260804193351-8cf7ccd5ee04

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/amir20/dozzle, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/amir20/dozzle to 1.29.1-0.20260804193351-8cf7ccd5ee04 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-73087 is resolved across your whole dependency graph.

  3. Workarounds

    Restrict outbound requests from the affected component to an allowlist of hosts, block access to link-local and internal address ranges at the network layer, and require authentication on internal services so a forged request cannot reach them unauthenticated.

Frequently Asked Questions

## 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 H
O3 Security · Impact-Aware SCA

Is CVE-2026-73087 in your dependencies?

Find it across Go, including transitive dependencies.

CVE-2026-73087: dozzle SSRF — Fix Available