SSRF

SSRF in Go (Gin): Secure Coding Guide [Apr 2026] [GHSA-xqq2-4j46-vwp7]

[Updated Mar 2026] Updated GHSA-xqq2-4j46-vwp7

Overview

SSRF, or Server-Side Request Forgery, is a real-world risk where an attacker can coerce the application into issuing HTTP requests to resources that the server can reach. In Go applications using the Gin framework, this commonly occurs when a handler accepts a user-supplied URL and fetches it server-side, enabling the attacker to access internal services, cloud metadata, or other protected endpoints from the host network. If unmitigated, an attacker could probe internal networks, reach private services, or exfiltrate data via the server's outbound connections. If left unchecked, SSRF can be used to enumerate internal services, access sensitive configurations, or exfiltrate data through the server’s outbound requests. In cloud environments, requests may target instance metadata services or private DNS names, potentially bypassing external access controls and compromising workloads behind firewalls. This vulnerability is particularly risky in microservices architectures where many services communicate over the network, and it can be triggered by simple query parameters or body payloads containing a URL. In Go with Gin, SSRF manifests when a route or handler takes a URL input (from query or body) and uses http.Get or a plain http.Client without validation or network restrictions. Timeouts, redirects, or lack of IP filtering enable abuse. The mitigation pattern is to validate and restrict outbound requests: limit to safe hosts, block private IPs, enforce timeouts, and route outbound traffic through a controlled egress point. This guide provides practical steps and a concrete code example showing vulnerable and fixed patterns. It assumes no CVEs are provided in this context but bases recommendations on common SSRF risk controls for Go (Gin) applications.

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "io/ioutil"
  "net"
  "net/http"
  "net/url"
  "time"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/vuln", handleVulnerable)
  r.GET("/fix", handleFixed)
  r.Run(":8080")
}

// Vulnerable pattern: directly fetch user-provided URL
func handleVulnerable(c *gin.Context) {
  target := c.Query("url")
  if target == "" {
    c.String(400, "missing url")
    return
  }
  resp, err := http.Get(target)
  if err != nil {
    c.String(500, err.Error())
    return
  }
  defer resp.Body.Close()
  body, _ := ioutil.ReadAll(resp.Body)
  c.Data(resp.StatusCode, "text/plain", body)
}

// Fixed pattern: validate URL, block private IPs, and use a bounded client
func handleFixed(c *gin.Context) {
  target := c.Query("url")
  if target == "" {
    c.String(400, "missing url")
    return
  }
  u, err := url.Parse(target)
  if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
    c.String(400, "invalid url")
    return
  }

  ips, err := net.LookupIP(u.Hostname())
  if err != nil || len(ips) == 0 {
    c.String(400, "unresolvable host")
    return
  }
  for _, ip := range ips {
    if isPrivateIP(ip) {
      c.String(403, "private IPs not allowed")
      return
    }
  }

  tr := &http.Transport{}
  client := &http.Client{Timeout: 5 * time.Second, Transport: tr}
  resp, err := client.Get(target)
  if err != nil {
    c.String(500, err.Error())
    return
  }
  defer resp.Body.Close()
  body, _ := ioutil.ReadAll(resp.Body)
  c.Data(resp.StatusCode, "text/plain", body)
}

func isPrivateIP(ip net.IP) bool {
  if ip == nil {
    return false
  }
  if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
    return true
  }
  if ip4 := ip.To4(); ip4 != nil {
    if ip4[0] == 10 {
      return true
    }
    if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 {
      return true
    }
    if ip4[0] == 192 && ip4[1] == 168 {
      return true
    }
    return false
  }
  // IPv6 private range fc00::/7
  if ip[0] == 0xfc || ip[0] == 0xfd {
    return true
  }
  return false
}

CVE References

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