SSRF

SSRF Mitigation Guide for Go (Gin) [GHSA-j432-4w3j-3w8j]

[Updated Sep 2026] Updated GHSA-j432-4w3j-3w8j

Overview

SSRF in Go applications using Gin can allow attackers to force your server to access internal services, cloud metadata endpoints, or other resources that are not publicly reachable. If an endpoint accepts a user-provided URL and then makes an outgoing request, the server becomes an intermediary that can be coerced into reaching sensitive destinations, potentially leaking credentials or enabling internal reconnaissance. In Gin-based services, SSRF commonly occurs when handlers proxy or fetch remote URLs based on user input, such as image fetchers, content proxies, or webhook integrations. If you call http.Get with a query parameter without validation, or forward the request through your server, you risk unintended access to internal networks or metadata endpoints. Remediation strategies include validating and constraining user input, implementing a host or URL allowlist, blocking private and non-routable IPs, and using a controlled HTTP client with strict timeouts. Additionally, enforce network segmentation, enable egress controls, and add thorough tests and monitoring for SSRF patterns. This guide provides a concrete example: vulnerable pattern that directly uses a user-supplied URL, and a fixed pattern that validates the URL, enforces an allowlist, blocks private IPs, and uses a safe HTTP client. Replace the vulnerable handler with the fixed one and verify with tests to prevent regressions.

Code Fix Example

Go (Gin) API Security Remediation
package main

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

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

func main() {
  r := gin.Default()
  r.GET("/fetch-vuln", vulnerableHandler)
  r.GET("/fetch-secure", fixedHandler)
  r.Run()
}

// VULNERABLE: directly uses user-supplied URL without validation
func vulnerableHandler(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, "request failed: %v", err)
    return
  }
  defer resp.Body.Close()
  body, _ := io.ReadAll(resp.Body)
  c.String(resp.StatusCode, string(body))
}

// FIX: validate URL, restrict to allowed hosts and non-private IPs
func fixedHandler(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
  }
  host := u.Hostname()
  if _, ok := allowedHosts[host]; !ok {
    c.String(403, "host not allowed")
    return
  }
  ips, err := net.LookupIP(host)
  if err != nil || len(ips) == 0 {
    c.String(400, "unable to resolve host")
    return
  }
  for _, ip := range ips {
    if isPrivateIP(ip) {
      c.String(403, "private IPs not allowed")
      return
    }
  }
  client := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}}
  resp, err := client.Get(target)
  if err != nil {
    c.String(500, "request failed: %v", err)
    return
  }
  defer resp.Body.Close()
  body, _ := io.ReadAll(resp.Body)
  c.String(resp.StatusCode, string(body))
}

func isPrivateIP(ip net.IP) bool {
  if ip.IsLoopback() {
    return true
  }
  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
    default:
      return false
    }
  }
  // IPv6 private ranges: fc00::/7 and fe80::/10
  if ip[0] == 0xFC || ip[0] == 0xFD {
    return true
  }
  if ip[0] == 0xFE && (ip[1]&0xC0) == 0x80 {
    return true
  }
  return false
}

CVE References

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