Overview
The CVE-2026-42449 family describes an SSRF vulnerability where an attacker can coerce a server-side component to perform HTTP requests to internal or cloud metadata endpoints by supplying a crafted URL. In n8n-MCP versions 2.47.4 through 2.47.13, the SDK embedder path and the synchronous URL validator (SSRFProtection.validateUrlSync()) did not properly handle IPv6 representations, including IPv4-mapped IPv6 addresses like http://[::ffff:169.254.169.254]. This allowed an attacker to bypass cloud metadata, RFC1918 private networks, and localhost checks, causing the server to fetch and return response bodies from targeted endpoints to the attacker. The vulnerability is non-blind SSRF since the response is returned to the caller, and the n8nApiKey is forwarded in a header to the attacker-controlled target. The issue was fixed in version 2.47.14. In Go (Gin) environments, a similar pattern can appear if URL validation only checks scheme and basic syntax without addressing IPv4-mapped IPv6 addresses, enabling remote fetches of internal resources when user-supplied URLs are proxied or fetched directly.
Affected Versions
2.47.4 through 2.47.13
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"io"
"net"
"net/http"
"net/url"
"time"
"github.com/gin-gonic/gin"
)
func isPrivateIP(ip net.IP) bool {
if ip == nil { return false }
if ip.To4() != nil {
p := ip.To4()
switch {
case p[0] == 10:
return true
case p[0] == 172 && p[1] >= 16 && p[1] <= 31:
return true
case p[0] == 192 && p[1] == 168:
return true
case p[0] == 169 && p[1] == 254:
return true
}
} else {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
return true
}
// IPv6 private ranges (ULA fc00::/7) and common local addresses
if ip[0] == 0xfc || ip[0] == 0xfd {
return true
}
}
return false
}
// Vulnerable pattern (no IP validation)
func vulnHandler(c *gin.Context) {
raw := c.Query("url")
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" {
c.String(400, "invalid url")
return
}
if u.Scheme != "http" && u.Scheme != "https" {
c.String(400, "unsupported scheme")
return
}
resp, err := http.Get(u.String())
if err != nil {
c.String(502, "fetch error")
return
}
defer resp.Body.Close()
c.Status(resp.StatusCode)
io.Copy(c.Writer, resp.Body)
}
// Fixed pattern (reject private/internal hosts and IPv4-mapped IPv6)
func fixHandler(c *gin.Context) {
raw := c.Query("url")
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" {
c.String(400, "invalid url")
return
}
if u.Scheme != "http" && u.Scheme != "https" {
c.String(400, "unsupported scheme")
return
}
host := u.Hostname()
ips, err := net.LookupIP(host)
if err != nil {
c.String(400, "host resolution failed")
return
}
for _, ip := range ips {
if isPrivateIP(ip) {
c.String(403, "private/internal host not allowed")
return
}
}
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(u.String())
if err != nil {
c.String(502, "fetch error")
return
}
defer resp.Body.Close()
c.Status(resp.StatusCode)
io.Copy(c.Writer, resp.Body)
}
func main() {
r := gin.Default()
r.GET("/vuln", vulnHandler)
r.GET("/fix", fixHandler)
r.Run(":8080")
}