SSRF

SSRF in Go Gin: Remediation Guide [Apr 2026] [GHSA-29v9-frvh-c426]

[Updated Apr 2026] Updated GHSA-29v9-frvh-c426

Overview

SSRF flaws in Go applications using the Gin framework allow an attacker to coerce the server into fetching URLs of their choice. If an endpoint accepts a URL from a client and uses the server as a client to request that URL, an attacker can reach internal services, cloud instance metadata endpoints, or other protected resources, potentially exfiltrating data or causing unintended actions. In multi-tenant or cloud environments, SSRF can enable lateral movement and lead to data breaches. Within Gin, SSRF typically appears when a route reads a user-supplied URL (for example from a query parameter) and immediately uses net/http to fetch that URL with http.Get or a shared http.Client. Because the server makes outbound requests with the application’s privileges, this can reach private networks or internal services, even if the user controls only part of the request. The vulnerability is usually a logic flaw rather than a library bug. Remediation involves validating and constraining destinations, and hardening outbound requests. Key practices include allowing only trusted hosts, validating the URL scheme, ensuring the destination is not a private IP, using a restricted HTTP client with timeouts and no or limited redirects, and avoiding fetching user-provided URLs unless strictly necessary. Consider routing such requests through a controlled service or proxy when possible. Operationally, implement observability, add tests with SSRF payloads, and review DevSecOps guidance to ensure new routes cannot bypass controls. After changes, verify by attempting payloads targeting private addresses, local metadata endpoints, and non-whitelisted hosts to confirm they are blocked.

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 ip4 := ip.To4(); ip4 != nil {
    switch ip4[0] {
    case 10:
      return true
    case 172:
      if ip4[1] >= 16 && ip4[1] <= 31 {
        return true
      }
    case 192:
      if ip4[1] == 168 {
        return true
      }
    }
  }
  return false
}

func main() {
  r := gin.Default()

  // Vulnerable: directly fetch user URL
  r.GET("/fetch/vuln", func(c *gin.Context) {
    raw := c.Query("url")
    resp, err := http.Get(raw)
    if err != nil {
      c.String(500, "request failed")
      return
    }
    defer resp.Body.Close()
    b, _ := io.ReadAll(resp.Body)
    c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), b)
  })

  // Fixed: validate URL, allowlist hosts, restrict IPs, and limit redirects
  r.GET("/fetch/fix", func(c *gin.Context) {
    raw := c.Query("url")
    u, err := url.Parse(raw)
    if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
      c.String(400, "invalid url")
      return
    }
    host := u.Hostname()
    allowed := map[string]struct{}{
      "example.com":    {},
      "api.example.com": {},
    }
    if _, ok := allowed[host]; !ok {
      c.String(403, "host not allowed")
      return
    }
    ips, err := net.LookupIP(host)
    if err != nil {
      c.String(500, "dns error")
      return
    }
    for _, ip := range ips {
      if isPrivateIP(ip) {
        c.String(403, "private ip not allowed")
        return
      }
    }
    client := &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
    resp, err := client.Get(raw)
    if err != nil {
      c.String(500, "request failed")
      return
    }
    defer resp.Body.Close()
    b, _ := io.ReadAll(resp.Body)
    c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), b)
  })

  r.Run(":8080")
}

CVE References

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