SSRF

SSRF in Go Gin remediation guide [May 2026] [GHSA-rpj4-7x2v-wjrf]

[Updated May 2026] Updated GHSA-rpj4-7x2v-wjrf

Overview

SSRF in Go Gin apps can allow an attacker-controlled URL to force your service to fetch internal resources. This can lead to sensitive data exposure, lateral movement within a cloud or container environment, and access to management interfaces that would otherwise be unreachable from the public network. In multi-tenant deployments, SSRF can enable an attacker to pivot between services sharing the same infrastructure. In Gin, SSRF surfaces when a handler reads a URL parameter from a request and uses the Go net/http client to fetch it without validating the target. Common patterns include image fetchers, content proxies, or webhook endpoints that dereference user-supplied URLs. If the target resolves to internal addresses (for example 10.x.x.x, 192.168.x.x, or metadata endpoints), requests may bypass access controls and leak data or facilitate further attacks. Remediation focuses on containment and verification: validate and constrain all outbound requests, avoid trust in user input, and implement robust egress controls. Core practices include using a host allowlist, restricting schemes, blocking private IPs, setting timeouts, enabling TLS verification, and centralizing HTTP calls behind a service layer with logging and metrics.

Code Fix Example

Go (Gin) API Security Remediation
package main

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

var allowedHosts = map[string]bool{
  "example.com":   true,
  "api.example.com": true,
}

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

func vulnerableFetch(target string) ([]byte, error) {
  resp, err := http.Get(target)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()
  return io.ReadAll(resp.Body)
}

func fixedFetch(target string) ([]byte, error) {
  u, err := url.Parse(target)
  if err != nil {
    return nil, err
  }
  if u.Scheme != "http" && u.Scheme != "https" {
    return nil, fmt.Errorf("unsupported URL scheme: %s", u.Scheme)
  }
  host := u.Hostname()
  if !allowedHosts[host] {
    return nil, fmt.Errorf("host not allowed: %s", host)
  }
  ips, err := net.LookupIP(host)
  if err != nil {
    return nil, err
  }
  for _, ip := range ips {
    if isPrivateIP(ip) {
      return nil, fmt.Errorf("private IPs are blocked")
    }
  }
  client := &http.Client{Timeout: 5 * time.Second}
  req, _ := http.NewRequest("GET", target, nil)
  resp, err := client.Do(req)
  if err != nil {
    return nil, err
  }
  defer resp.Body.Close()
  return io.ReadAll(resp.Body)
}

func main() {
  r := gin.Default()
  r.GET("/vuln", func(c *gin.Context) {
    t := c.Query("url")
    if t == "" {
      c.String(400, "url is required")
      return
    }
    data, err := vulnerableFetch(t)
    if err != nil {
      c.String(500, err.Error())
      return
    }
    c.Data(200, "text/plain", data)
  })
  r.GET("/fixed", func(c *gin.Context) {
    t := c.Query("url")
    if t == "" {
      c.String(400, "url is required")
      return
    }
    data, err := fixedFetch(t)
    if err != nil {
      c.String(500, err.Error())
      return
    }
    c.Data(200, "text/plain", data)
  })
  r.Run(":8080")
}

CVE References

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