SSRF

SSRF in Go (Gin) Remediation Guide

[Updated March 2026] Updated

Overview

SSRF vulnerabilities in Go (Gin) apps can enable attackers to trigger outbound requests from your server to internal networks or cloud metadata services. This can compromise sensitive services, bypass network segmentation, or cause data exfiltration, outages, or internal scanning. In Gin, SSRF typically manifests when handlers accept a user-supplied URL for fetching resources, proxying data, or redirection and then perform HTTP requests without validating the target. Because Go's default HTTP client can honor proxies and environment settings, an attacker might also pivot through proxies to reach internal endpoints. This class of vulnerabilities can be introduced by simple patterns like fetch endpoints that take a URL parameter or image/proxy handlers that trust client input. Remediation should include validating and sanitizing user input, implementing an allowlist of permitted hosts/schemes, disallowing private IP ranges, and explicitly disabling outbound proxies by configuring the HTTP client transport with Proxy: nil. Add timeouts, isolate outbound requests behind a gateway with strict egress rules, and write tests and monitoring for disallowed targets. No CVEs provided (N/A).

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "crypto/tls"
  "fmt"
  "io"
  "net/http"
  "net/url"
  "strings"
  "time"

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

const (
  errUnsupportedScheme = `unsupported scheme: %s`
  errHostNotAllowed    = `host not allowed: %s`
)

func fetchVulnerable(target string) (string, error) {
  resp, err := http.Get(target)
  if err != nil {
    return "", err
  }
  defer resp.Body.Close()
  b, err := io.ReadAll(resp.Body)
  if err != nil {
    return "", err
  }
  return string(b), nil
}

func fetchSecure(target string) (string, error) {
  u, err := url.Parse(target)
  if err != nil {
    return "", err
  }
  if u.Scheme != "http" && u.Scheme != "https" {
    return "", fmt.Errorf(errUnsupportedScheme, u.Scheme)
  }
  hostOk := false
  allowed := []string{`example.com`, `api.example.com`}
  for _, h := range allowed {
    if strings.HasSuffix(strings.ToLower(u.Hostname()), h) {
      hostOk = true
      break
    }
  }
  if !hostOk {
    return "", fmt.Errorf(errHostNotAllowed, u.Hostname())
  }

  tr := &http.Transport{Proxy: nil, TLSClientConfig: &tls.Config{InsecureSkipVerify: false}}
  client := &http.Client{Transport: tr, Timeout: 5 * time.Second}
  resp, err := client.Get(target)
  if err != nil {
    return "", err
  }
  defer resp.Body.Close()
  b, err := io.ReadAll(resp.Body)
  if err != nil {
    return "", err
  }
  return string(b), nil
}

func main() {
  r := gin.Default()
  r.GET("/vuln", func(c *gin.Context) {
    u := c.Query(`url`)
    body, err := fetchVulnerable(u)
    if err != nil {
      c.String(400, err.Error())
      return
    }
    c.String(200, body)
  })
  r.GET("/secure", func(c *gin.Context) {
    u := c.Query(`url`)
    body, err := fetchSecure(u)
    if err != nil {
      c.String(400, err.Error())
      return
    }
    c.String(200, body)
  })
  r.Run(`:8080`)
}

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