Overview
SSRF vulnerabilities can enable attackers to reach internal services, cloud metadata endpoints, or private networks from your server. In real-world deployments, such flaws can lead to credentials exposure, unintended data exfiltration, or lateral movement within a cloud or on-prem network when an API endpoint fetches a URL supplied by a client.
In Go with the Gin framework, SSRF typically happens when a route accepts a URL parameter or body field and uses net/http to fetch that URL without strict validation. Even seemingly harmless proxy or fetch endpoints can become an attack surface if the destination is not properly constrained.
Remediation involves input validation, network egress controls, and a hardened HTTP client. Use an allowlist or robust host/IP checks, validate the URL and scheme, block private IP ranges, implement timeouts and limited redirects, and consider performing the fetch in a controlled, isolated service.
Testing, monitoring, and defense in depth: add unit tests for blocked URLs, monitor SSRF attempts, and ensure outbound access is restricted to approved endpoints only.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"io/ioutil"
"net"
"net/http"
"net/url"
"time"
"strings"
"github.com/gin-gonic/gin"
)
func vulnerableHandler(c *gin.Context) {
urlStr := c.Query("url")
resp, err := http.Get(urlStr)
if err != nil {
c.String(500, "request failed")
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
c.Data(200, "text/plain", body)
}
func isPrivateIP(ip net.IP) bool {
privateCIDRs := []string{"10.0.0.0/8","172.16.0.0/12","192.168.0.0/16","127.0.0.0/8"}
for _, cidr := range privateCIDRs {
_, block, _ := net.ParseCIDR(cidr)
if block.Contains(ip) {
return true
}
}
return ip.IsLoopback()
}
func fixedHandler(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()
if ips, err := net.LookupIP(host); err == nil {
for _, ip := range ips {
if isPrivateIP(ip) {
c.String(400, "blocked internal host")
return
}
}
}
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
}
resp, err := client.Get(raw)
if err != nil {
c.String(502, "fetch failed")
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
c.Data(200, "text/plain", body)
}
func main() {
r := gin.Default()
r.GET("/fetch", vulnerableHandler)
r.GET("/secure-fetch", fixedHandler)
r.Run(":8080")
}