SSRF

SSRF in Go (Gin): Secure Coding Guide [Mar 2026] [GHSA-jh46-85jr-6ph9]

[Updated Mar 2026] Updated GHSA-jh46-85jr-6ph9

Overview

SSRF vulnerabilities enable an attacker to trick the server into making HTTP requests on their behalf. In Go (Gin) applications, an endpoint that fetches a URL supplied by a client can be abused to reach internal services, cloud metadata endpoints, or other sensitive resources that are not meant to be exposed to the wider network. Depending on the environment, this can lead to data exposure, privilege escalation within a private network, or access to service discovery endpoints. In practice, SSRF in Go with Gin often shows up in endpoints that proxy requests, fetch remote images, or implement dynamic backends based on a user-provided URL. If input is not validated or restricted, an attacker can direct requests to localhost, internal container networks, or metadata services, potentially leaking credentials or triggering denial of service. Go's standard library makes it easy to perform server-side requests, so developers must be explicit about what can be fetched and from where. This guide demonstrates safe patterns using URL parsing, host allowlists, and IP checks, and it highlights how to structure code to minimize exposure in a Gin-based service. Along with code changes, teams should add monitoring, timeouts, and explicit testing for SSRF vectors. The remediation steps below outline concrete steps to detect, fix, and prevent SSRF in Go (Gin) services.

Code Fix Example

Go (Gin) API Security Remediation
package main

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

func isPrivateIP(ip net.IP) bool {
  if ip == nil { return true }
  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
    }
  }
  return false
}

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

  // Vulnerable pattern: using user-supplied URL directly
  r.GET(`/fetch-vuln`, func(c *ggin.Context) {
    target := c.Query(`url`)
    if target == `` {
      c.String(400, `missing url parameter`)
      return
    }
    resp, err := http.Get(target)
    if err != nil {
      c.String(502, err.Error())
      return
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    c.Data(resp.StatusCode, `text/plain; charset=utf-8`, body)
  })

  // Fixed pattern: validate URL, allowlist hostnames and block private IPs
  r.GET(`/fetch-fixed`, func(c *ggin.Context) {
    target := c.Query(`url`)
    if target == `` {
      c.String(400, `missing url parameter`)
      return
    }
    u, err := url.ParseRequestURI(target)
    if err != nil || (u.Scheme != `http` && u.Scheme != `https`) {
      c.String(400, `invalid URL`)
      return
    }
    host := u.Hostname()
    allowed := map[string]bool{ `example.com`: true, `api.example.org`: true }
    if !allowed[host] {
      c.String(403, `host not allowed`)
      return
    }
    addrs, err := net.LookupIP(host)
    if err == nil {
      for _, a := range addrs {
        if isPrivateIP(a) {
          c.String(403, `private IPs are not allowed`)
          return
        }
      }
    }
    client := &http.Client{ Timeout: 5 * time.Second }
    resp, err := client.Get(target)
    if err != nil {
      c.String(502, err.Error())
      return
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    c.Data(resp.StatusCode, `text/plain; charset=utf-8`, body)
  })

  r.Run(`:8080`)
}

CVE References

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