SSRF

SSRF in Go (Gin) remediation guide [Mar 2026] [GHSA-66cw-h2mj-j39p]

[Updated Mar 2026] Updated GHSA-66cw-h2mj-j39p

Overview

SSRF vulnerabilities in Go applications using the Gin web framework can let an attacker trick the server into fetching resources they control or that are only reachable from inside the network. By supplying a URL parameter or by abusing a proxy endpoint, an attacker can reach internal services, cloud instance metadata, or admin interfaces, potentially leaking sensitive data or enabling further intrusion. In practice, this flaw often arises when a Gin route accepts a user-provided URL and performs an HTTP request or when a misconfigured proxy surface is exposed. In Go/Gin, SSRF typically manifests through endpoints that fetch remote content based on user input, such as an image proxy or fetcher. Even with static code, a careless use of the default HTTP client may follow redirects to internal hosts or resolve private IP addresses, enabling access to internal networks or metadata endpoints. Without explicit validation, attackers can probe the internal surface, test reachability, and exfiltrate responses from internal services. Remediation approach: implement input validation, restrict destinations, and enforce network boundaries. Use an allowlist of domains, require http/https schemes, validate hosts and IPs against private ranges, resolve DNS to detect internal addresses, and set tight request timeouts. Use a dedicated, hardened HTTP client with a proper redirect policy and consider isolating SSRF-facing logic behind a proxy service. Add tests, security checks, and runtime monitoring for SSRF indicators.

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "errors"
  "io"
  "net"
  "net/http"
  "net/url"
  "time"
  "github.com/gin-gonic/gin"
)

// Vulnerable pattern: directly fetch user-controlled URL
func fetchVulnerable(urlStr string) ([]byte, error) {
  resp, err := http.Get(urlStr)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()
  return io.ReadAll(resp.Body)
}

// Helpers for a safe fetcher
var allowlistedHosts = map[string]bool{
  "example.com":     true,
  "api.example.com": true,
}

func isAllowedHost(host string) bool {
  return allowlistedHosts[host]
}

func isPrivateIP(ip net.IP) bool {
  if ip == nil {
    return false
  }
  if ip4 := ip.To4(); ip4 != nil {
    switch {
    case ip4[0] == 10:
      return true
    case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
      return true
    case ip4[0] == 192 && ip4[1] == 168:
      return true
    }
  }
  return false
}

// Fixed: validates URL, host allowlist, and uses a restricted client
func fetchFixed(urlStr string) ([]byte, error) {
  u, err := url.Parse(urlStr)
  if err != nil {
    return nil, err
  }
  if u.Scheme != "http" && u.Scheme != "https" {
    return nil, errors.New("unsupported URL scheme")
  }
  host := u.Hostname()
  if !isAllowedHost(host) {
    return nil, errors.New("host not allowed")
  }
  // DNS resolution + private IP check
  ips, err := net.LookupIP(host)
  if err != nil {
    return nil, err
  }
  for _, ip := range ips {
    if isPrivateIP(ip) {
      return nil, errors.New("private IPs not allowed")
    }
  }
  client := http.Client{
    Timeout: 5 * time.Second,
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
      // Do not follow redirects to hidden/internal hosts
      return http.ErrUseLastResponse
    },
  }
  resp, err := client.Get(urlStr)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()
  return io.ReadAll(resp.Body)
}

func main() {
  r := gin.Default()
  r.GET("/proxy", func(c *gin.Context) {
    urlStr := c.Query("url")
    // Vulnerable usage example
    // data, _ := fetchVulnerable(urlStr)
    // Fixed usage example
    data, err := fetchFixed(urlStr)
    if err != nil {
      c.String(400, "error: %v", err)
      return
    }
    c.Data(http.StatusOK, "application/octet-stream", data)
  })
  r.Run()
}

CVE References

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