SSRF

SSRF in Go Gin remediation guide [GHSA-rh5x-h6pp-cjj6]

[Updated May 2026] Updated GHSA-rh5x-h6pp-cjj6

Overview

SSRF (Server-Side Request Forgery) vulnerabilities in Go applications using the Gin framework arise when an endpoint accepts a URL from a client and fetches it server-side. An attacker can instruct the server to reach internal or protected resources, expose sensitive metadata, or pivot through a private network. This class of flaws is particularly dangerous in cloud environments where the instance has access to internal services or metadata endpoints. In Gin, SSRF typically manifests as a handler that reads a URL from query parameters or JSON body and uses http.Get or an http.Client to fetch that URL without validating the destination. Because Go automatically respects environment proxies and DNS resolution, an attacker can steer requests toward internal addresses, cloud metadata endpoints, or other restricted networks if no checks are performed. This can lead to information disclosure, port scanning, or abuse of internal services. Remediation focuses on origin whitelist and network egress controls, using safe HTTP patterns and input validation. Validate and constrain the destination URL, prohibit private or loopback addresses, disable proxies for outbound requests, set timeouts, and avoid returning remote content directly without validation. Use a dedicated, least-privilege fetcher component for any remote resources. Additionally, monitor and test for SSRF, apply defense-in-depth (WAF, network ACLs), and consider architectural patterns such as routing requests through a controlled gateway service. The accompanying example contrasts a vulnerable handler with a fixed one and demonstrates how to implement a safe outbound fetch in Go Gin.

Code Fix Example

Go (Gin) API Security Remediation
package main

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

func main() {
  r := gin.Default()
  r.GET("/vulnerable", vulnerableHandler)
  r.GET("/fixed", fixedHandler)
  r.Run(":8080")
}

// VULNERABLE: user-controlled URL used directly
func vulnerableHandler(c *gin.Context) {
  target := c.Query("target")
  if target == "" {
    c.String(400, "target is required")
    return
  }
  resp, err := http.Get(target)
  if err != nil {
    c.String(502, "upstream error: %v", err)
    return
  }
  defer resp.Body.Close()
  body, _ := ioutil.ReadAll(resp.Body)
  c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}

// FIXED: allowlist, IP checks, restricted client
var allowedHosts = map[string]struct{}{
  "example.com":     {},
  "api.example.com": {},
}

func isAllowedHost(hostport string) bool {
  host, _, err := net.SplitHostPort(hostport)
  if err != nil {
    host = hostport
  }
  host = strings.ToLower(host)
  if _, ok := allowedHosts[host]; ok {
    return true
  }
  if strings.HasSuffix(host, ".example.com") {
    return true
  }
  return false
}

func isPrivateIP(ip net.IP) bool {
  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
    case ip4[0] == 127:
      return true
    }
  }
  return false
}

func fixedHandler(c *gin.Context) {
  target := c.Query("target")
  if target == "" {
    c.String(400, "target is required")
    return
  }
  u, err := url.Parse(target)
  if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
    c.String(400, "invalid URL")
    return
  }
  if !isAllowedHost(u.Host) {
    c.String(403, "host not allowed")
    return
  }
  ips, err := net.LookupIP(u.Host)
  if err == nil {
    for _, ip := range ips {
      if isPrivateIP(ip) {
        c.String(403, "internal IP not allowed")
        return
      }
    }
  }
  client := &http.Client{
    Timeout: 5 * time.Second,
    Transport: &http.Transport{ Proxy: nil }, // disable environment proxies
  }
  resp, err := client.Get(target)
  if err != nil {
    c.String(502, "upstream error: %v", err)
    return
  }
  defer resp.Body.Close()
  body, _ := ioutil.ReadAll(resp.Body)
  c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}

CVE References

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