CVE-2026-73087: Dozzle SSRF Guard Bypass Vulnerability

LOW
Advisory Identifier: GHSA-p2w3-6x73-2f6x
CWE-918
Published
Advisory Summary

CVE-2026-73087 is a LOW ssrf guard bypass vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Dozzle libraries in the Go ecosystem. The issue involves ssrf guard bypass via ipv6 transition addresses (6to4/nat64/teredo) in webhook notification dispatcher. Remediation is available by upgrading affected packages to version 1.29.1-0.20260804193351-8cf7ccd5ee04.

CVE-2026-73087 at a Glance

CVE Identifier CVE-2026-73087
GHSA Identifier GHSA-p2w3-6x73-2f6x
Severity LOW
CVSS v3.1 Base Score N/A
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Dozzle
Package Ecosystem Go
Published Date
Last Updated

How to Fix CVE-2026-73087

To remediate this vulnerability, upgrade all affected Dozzle dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/amir20/dozzle fixed: 1.29.1-0.20260804193351-8cf7ccd5ee04
Upgrade github.com/amir20/dozzle to 1.29.1-0.20260804193351-8cf7ccd5ee04
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/amir20/dozzle

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-73087, inspect your project dependencies for Dozzle:

inspect dependencies for github.com/amir20/dozzle

Analyze the terminal output to locate any version of Dozzle in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.29.1-0.20260804193351-8cf7ccd5ee04), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Dozzle, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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`](https://github.com/amir20/dozzle/commit/b9df31356fc024e6768a1aac605844dc041d95ee)) - Vulnerable code: `internal/notification/dispatcher/webhook.go` ## Details ### Root cause (CWE-918) [`internal/notification/dispatcher/webhook.go:32-51`](https://github.com/amir20/dozzle/blob/b9df31356fc024e6768a1aac605844dc041d95ee/internal/notification/dispatcher/webhook.go#L32-L51): ```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).

CVE-2026-73087 Affected Dozzle Versions

1 component(s)
Fixed Versions:
1.29.1-0.20260804193351-8cf7ccd5ee04
Product / Package Ecosystem Affected Versions Fixed Version
github.com/amir20/dozzle
Go
< 1.29.1-0.20260804193351-8cf7ccd5ee04
1.29.1-0.20260804193351-8cf7ccd5ee04

References

Frequently Asked Questions

What is CVE-2026-73087?
CVE-2026-73087 is a security vulnerability in Dozzle involving SSRF guard bypass via IPv6 transition addresses (6to4/NAT64/Teredo) in webhook notification dispatcher under CWE-918: Server-Side Request Forgery (SSRF).
Which Dozzle versions are affected?
Affected versions include releases prior to 1.29.1-0.20260804193351-8cf7ccd5ee04 across affected packages: github.com/amir20/dozzle.
What version fixes CVE-2026-73087?
Upgrading to 1.29.1-0.20260804193351-8cf7ccd5ee04 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/amir20/dozzle affected?
Yes, github.com/amir20/dozzle is affected in releases prior to 1.29.1-0.20260804193351-8cf7ccd5ee04. Upgrade your dependency to 1.29.1-0.20260804193351-8cf7ccd5ee04 or later.
What CWE is associated with CVE-2026-73087?
CVE-2026-73087 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check Go dependencies for CVE-2026-73087?
Run the dependency tree command: 'inspect dependencies for github.com/amir20/dozzle'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-p2w3-6x73-2f6x",
  "modified" : "2026-09-08T18:11:56Z",
  "published" : "2026-09-08T18:11:56Z",
  "aliases" : [ "CVE-2026-73087" ],
  "summary" : "Dozzle: SSRF guard bypass via IPv6 transition addresses (6to4/NAT64/Teredo) in webhook notification dispatcher",
  "details" : "## Summary\n\nThe `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.\n\n## Affected component / versions\n\n- Package: `github.com/amir20/dozzle`\n- Affected versions: all versions with SSRF guard (current HEAD [`b9df313`](https://github.com/amir20/dozzle/commit/b9df31356fc024e6768a1aac605844dc041d95ee))\n- Vulnerable code: `internal/notification/dispatcher/webhook.go`\n\n## Details\n\n### Root cause (CWE-918)\n\n[`internal/notification/dispatcher/webhook.go:32-51`](https://github.com/amir20/dozzle/blob/b9df31356fc024e6768a1aac605844dc041d95ee/internal/notification/dispatcher/webhook.go#L32-L51):\n\n```go\nfunc isBlockedIP(ip net.IP) bool {\n\tif ip.IsLoopback() ||\n\t\tip.IsLinkLocalUnicast() ||\n\t\tip.IsLinkLocalMulticast() ||\n\t\tip.IsMulticast() ||\n\t\tip.IsInterfaceLocalMulticast() ||\n\t\tip.IsUnspecified() {\n\t\treturn true\n\t}\n\tif v4 := ip.To4(); v4 != nil && zeroNetV4.Contains(v4) {\n\t\treturn true\n\t}\n\tif ip.Equal(net.IPv4bcast) {\n\t\treturn true\n\t}\n\treturn false\n}\n```\n\nThe 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:\n\n| Mechanism | Prefix | Embeds | `isBlockedIP` result |\n|-----------|--------|--------|---------------------|\n| 6to4 | `2002::/16` | any IPv4 in bits 16-47 | `false` |\n| NAT64 WKP | `64:ff9b::/96` | any IPv4 in bits 96-127 | `false` |\n| Teredo | `2001:0000::/32` | any IPv4 in bits 96-127 | `false` |\n\n### Reachability / trust boundary\n\nThe `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).\n\nWebhook 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.\n\n### Attack chain\n\n1. Authenticated user creates a webhook notification with URL `http://[2002:7f00:0001::1]:8080/` (6to4 embedding 127.0.0.1)\n2. When a notification triggers, Dozzle's webhook dispatcher calls `safeDialContext`\n3. The IPv6 address `2002:7f00:0001::1` is checked against `isBlockedIP` -- all predicates return false\n4. Connection proceeds to the 6to4 relay which routes to 127.0.0.1\n5. The webhook POST reaches the host's loopback services\n\n## Impact\n\nAn authenticated user can bypass the SSRF guard to:\n\n- Reach cloud metadata service at 169.254.169.254 via `2002:a9fe:a9fe::1` (6to4) to steal instance credentials\n- Reach localhost services via `64:ff9b::7f00:1` (NAT64) or `2002:7f00:0001::1` (6to4)\n- 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)\n\nNote: 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.\n\n## Proof of concept\n\nBypass vectors:\n\n```\n# 6to4 embedding 127.0.0.1 (bypasses IsLoopback)\nhttp://[2002:7f00:0001::1]:8080/\n\n# NAT64 embedding 169.254.169.254 (bypasses IsLinkLocalUnicast)\nhttp://[64:ff9b::a9fe:a9fe]/latest/meta-data/\n\n# 6to4 embedding 169.254.169.254 (bypasses IsLinkLocalUnicast)\nhttp://[2002:a9fe:a9fe::1]/latest/meta-data/\n\n# Teredo embedding 127.0.0.1\nhttp://[2001:0000:dead:beef:0000:0000:7f00:0001]:8080/\n```\n\nVerification that `isBlockedIP` returns false for all vectors:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"net\"\n)\n\nfunc isBlockedIP(ip net.IP) bool {\n    return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||\n        ip.IsMulticast() || ip.IsInterfaceLocalMulticast() || ip.IsUnspecified()\n}\n\nfunc main() {\n    for _, v := range []string{\n        \"2002:7f00:0001::1\",    // 6to4 -> 127.0.0.1\n        \"64:ff9b::a9fe:a9fe\",   // NAT64 -> 169.254.169.254\n        \"2002:a9fe:a9fe::1\",    // 6to4 -> 169.254.169.254\n    } {\n        ip := net.ParseIP(v)\n        fmt.Printf(\"%-35s blocked=%v\\n\", v, isBlockedIP(ip))\n    }\n}\n// Output: all false\n```\n\n## Remediation\n\nAdd IPv6 transition mechanism prefix checks to `isBlockedIP`:\n\n```go\nfunc isBlockedIP(ip net.IP) bool {\n\t// ... existing checks ...\n\n\t// IPv6 transition mechanisms embedding arbitrary IPv4\n\tif len(ip) == net.IPv6len {\n\t\tif ip[0] == 0x20 && ip[1] == 0x02 { return true } // 6to4\n\t\tif ip[0] == 0x00 && ip[1] == 0x64 && ip[2] == 0xff && ip[3] == 0x9b { return true } // NAT64\n\t\tif ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0x00 && ip[3] == 0x00 { return true } // Teredo\n\t}\n\treturn false\n}\n```\n\n## Credit\n\nReported by tonghuaroot (tonghuaroot@gmail.com).",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/amir20/dozzle"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.29.1-0.20260804193351-8cf7ccd5ee04"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/security/advisories/GHSA-p2w3-6x73-2f6x"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-73087"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/pull/4887"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/commit/8cf7ccd5ee041ecaea92b49951793d4d2393761f"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/amir20/dozzle"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/amir20/dozzle/releases/tag/v10.6.15"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-918" ],
    "severity" : "LOW",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-08T18:11:56Z",
    "nvd_published_at" : "2026-08-11T17:19:16Z"
  }
}