Overview
SSRF vulnerabilities allow an attacker to make your server fetch arbitrary resources. In cloud environments or internal networks, this can reach metadata services (like cloud provider metadata), internal services, or restricted databases. Attackers can use your app as a proxy to scan networks, exfiltrate data, or access services not exposed to the public internet. In worst cases this leads to privilege escalation, data leakage, or unauthorized access to sensitive systems.
In Go with the Gin framework, SSRF commonly manifests when an endpoint accepts a URL or path from a client (for example via query parameters or JSON body) and then uses the net/http client to fetch that URL (http.Get, Client.Do, etc.). If input is not validated, an attacker can point the server at internal addresses (http://169.254.169.254/ for cloud metadata, http://10.0.0.0/8, etc.). The framework does not mitigate this by default; it's about how you implement outbound requests.
Remediation involves validating and normalizing URLs, implementing allowlists, restricting schemes, blocking internal addresses, adding timeouts and redirect limits, and isolating the fetch logic behind a service boundary.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Vulnerable endpoint: uses user-provided URL directly
r.GET("/fetch/vulnerable", func(c *gin.Context) {
target := c.Query("url")
resp, err := http.Get(target) // SSRF risk: attacker controls URL
if err != nil {
c.String(400, "request error")
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
})
// Fixed endpoint: validates URL, enforces allowlist and internal IP checks
r.GET("/fetch/fixed", func(c *gin.Context) {
target := c.Query("url")
parsed, err := url.Parse(target)
if err != nil {
c.String(400, "invalid url")
return
}
// Enforce allowed schemes
if parsed.Scheme != "http" && parsed.Scheme != "https" {
c.String(400, "unsupported URL scheme")
return
}
host := parsed.Hostname()
// Simple host allowlist
allowed := map[string]bool{
"api.example.com": true,
"service.example.com": true,
}
if !allowed[host] {
c.String(403, "host not allowed")
return
}
// Optional: prevent internal targets by resolving to IPs and blocking private ranges
ips, err := net.LookupIP(host)
if err == nil {
for _, ip := range ips {
if isPrivateIP(ip) {
c.String(403, "internal host not allowed")
return
}
}
}
// Safe fetch with same allowlist as guardrails
resp, err := http.Get(target)
if err != nil {
c.String(400, "request error")
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
})
r.Run(":8080")
}
func isPrivateIP(ip net.IP) bool {
if ip == nil {
return false
}
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
case ip4.IsLoopback():
return true
}
} else {
if ip.IsLoopback() {
return true
}
}
return false
}