Overview
Server-side request forgery (SSRF) is a class of vulnerabilities where a web server fetches resources based on client input. In Go apps built with Gin, endpoints that proxy a client-supplied URL, or endpoints that fetch remote data for rendering, can be abused to reach internal services, cloud metadata endpoints, or services behind a firewall. Successful exploitation can lead to data leakage, access to private resources, or abuse of internal networks to pivot to other hosts.
In Go (Gin) this class of vulnerability often manifests when developers use net/http directly to fetch a URL supplied by the user (via query parameters or form data) without validating the target. If redirects are followed, or if the server uses a reverse proxy without isolation, an attacker can force requests to internal addresses or restricted interfaces. Typical patterns include a /proxy or /fetch endpoint that accepts a URL, or a handler that streams remote content to the client.
Impact is highly contextual: in cloud environments, SSRF can access 169.254.169.254 metadata services, internal dashboards, or internal services. It also risks information disclosure, excessive bandwidth consumption, or abuse of the hosting environment. While Gin itself doesn't introduce a vulnerability, careless use of Go's http client with user-controlled URLs creates a broad attack surface.
Mitigation requires input validation, network-bound controls, and code-level controls. The goal is to ensure the server only fetches resources you intend and cannot reach private networks or internal endpoints. The following snippet shows a vulnerable pattern and a safe alternative.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/vuln/proxy", vulnerableProxy)
r.GET("/fix/proxy", safeProxy)
r.Run()
}
// Vulnerable pattern: proxies user-supplied URL without validation
func vulnerableProxy(c *gin.Context) {
target := c.Query("url")
resp, err := http.Get(target)
if err != nil { c.String(500, "error: %v", err); return }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}
// Safe version: validates URL, uses allowlist, and restricts redirects
func safeProxy(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 !isAllowedHost(host) {
c.String(403, "host not allowed"); return
}
if ip := net.ParseIP(host); ip != nil && isPrivateIP(ip) {
c.String(403, "private IP not allowed"); return
}
client := &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error {
if !isAllowedHost(req.URL.Hostname()) { return http.ErrUseLastResponse }
return nil
}}
resp, err := client.Get(u.String())
if err != nil { c.String(500, "error: %v", err); return }
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), body)
}
func isAllowedHost(h string) bool {
allowed := []string{"example.com", "api.myservice.local"}
hl := strings.ToLower(h)
for _, a := range allowed {
if hl == strings.ToLower(a) { return true }
}
return false
}
func isPrivateIP(ip net.IP) bool {
if ip.IsLoopback() { return true }
if ip.To4() != nil {
for _, cidr := range []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"} {
_, netw, _ := net.ParseCIDR(cidr)
if netw.Contains(ip) {
return true
}
}
return false
}
for _, cidr := range []string{"fc00::/7", "fd00::/8"} {
_, netw, _ := net.ParseCIDR(cidr)
if netw.Contains(ip) { return true }
}
return false
}