SSRF

SSRF in Go Gin: Fix IPv6-mapped URL validation [CVE-2026-42449]

[Fixed 2026-07] Updated CVE-2026-42449

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")
}

CVE References

Choose which optional cookies to allow. You can change this any time.