Overview
SSRF (Server-Side Request Forgery) vulnerabilities let an attacker coerce your server into making HTTP requests to arbitrary destinations. In cloud or container environments, this can expose internal services, reach metadata endpoints, or access restricted resources, potentially exposing sensitive data or enabling lateral movement within a network. Exploitation often leads to data exfiltration, discovery of internal services, or abuse of downstream systems that trust the server’s outbound requests. In Go applications using the Gin framework, SSRF commonly occurs when handlers take a user-supplied URL and fetch it server-side using the default HTTP client, with insufficient input validation or network containment. If egress is broadly allowed, an attacker may reach internal networks or cloud metadata endpoints, amplifying the impact of the flaw.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"fmt"
"io/ioutil"
"net"
"net/http"
neturl "net/url"
"time"
"github.com/gin-gonic/gin"
)
// Vulnerable pattern: directly using user input to fetch an arbitrary URL
func vulnerableFetch(target string) ([]byte, error) {
resp, err := http.Get(target)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
// Fixed pattern: validate input, restrict to https, block private/internal IPs,
// and use a restricted HTTP client (timeouts, no proxies)
func secureFetch(target string) ([]byte, error) {
u, err := neturl.Parse(target)
if err != nil {
return nil, err
}
if u.Scheme != "https" {
return nil, fmt.Errorf("unsupported scheme: %s", u.Scheme)
}
// Resolve hostname and ensure it is not private/internal
ips, err := net.LookupIP(u.Hostname())
if err != nil {
return nil, err
}
for _, ip := range ips {
if isPrivateIP(ip) {
return nil, fmt.Errorf("private/internal host not allowed: %s", ip.String())
}
}
// HTTP client with strict timeout and no proxies
tr := &http.Transport{
Proxy: func(*http.Request) (*neturl.URL, error) { return nil, nil }, // disable proxies
}
client := &http.Client{
Timeout: 5 * time.Second,
Transport: tr,
}
resp, err := client.Get(target)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func isPrivateIP(ip net.IP) bool {
if ip == nil {
return false
}
ip4 := ip.To4()
if ip4 == nil {
return false
}
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[0] == 127:
return true
default:
return false
}
}
func main() {
r := gin.Default()
r.GET("/fetch", func(c *gin.Context) {
t := c.Query("target")
mode := c.Query("mode")
if t == "" {
c.String(400, "target parameter required")
return
}
var data []byte
var err error
if mode == "vuln" {
data, err = vulnerableFetch(t)
} else {
data, err = secureFetch(t)
}
if err != nil {
c.String(500, err.Error())
return
}
c.Data(200, "application/octet-stream", data)
})
_ = r.Run(":8080")
}