flawopen.com/Server-Side Request Forgery/Go
You send a messenger with an envelope. You tell him, 'Deliver this to whoever the address says.' The sender writes 'Room 102 next door'. The messenger delivers to your private internal server instead of the public internet.
Go's http.DefaultClient.Get(url) resolves DNS and connects without restriction. If an attacker submits http://169.254.169.254 or http://127.0.0.1:8080, Go connects and returns the response.
In 2022, critical SSRF flaws in Kubernetes operators and cloud webhook integrations written in Go enabled lateral movement and cluster administrative privilege escalation.
CISA Cybersecurity Advisory & MITRE CVE repository.// VULNERABLE: Default http.Get allows internal network calls
package main
import (
"io"
"net/http"
)
func proxyHandler(w http.ResponseWriter, r *http.Request) {
targetURL := r.URL.Query().Get("url")
// Attacker passes: http://169.254.169.254/latest/meta-data/
resp, err := http.Get(targetURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
io.Copy(w, resp.Body)
}
// HARDENED: Custom DialContext checking net.IP.IsPrivate and IsLoopback
package main
import (
"context"
"errors"
"io"
"net"
"net/http"
"net/url"
"time"
)
func isSafeIP(ip net.IP) bool {
return !ip.IsLoopback() &&
!ip.IsPrivate() &&
!ip.IsLinkLocalUnicast() &&
!ip.IsLinkLocalMulticast() &&
!ip.IsUnspecified()
}
func newSafeHTTPClient() *http.Client {
dialer := &net.Dialer{
Timeout: 3 * time.Second,
}
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
// Resolve all IP addresses
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
if err != nil {
return nil, err
}
for _, ip := range ips {
if !isSafeIP(ip) {
return nil, errors.New("SSRF Blocked: connection to private/internal IP rejected")
}
}
// Connect to the first validated IP
targetAddr := net.JoinHostPort(ips[0].String(), port)
return dialer.DialContext(ctx, network, targetAddr)
},
}
return &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return errors.New("redirects are disabled to prevent SSRF bypass")
},
}
}
func proxyHandler(w http.ResponseWriter, r *http.Request) {
targetURL := r.URL.Query().Get("url")
parsed, err := url.Parse(targetURL)
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
http.Error(w, "Invalid URL scheme", http.StatusBadRequest)
return
}
client := newSafeHTTPClient()
resp, err := client.Get(targetURL)
if err != nil {
http.Error(w, "Request failed: "+err.Error(), http.StatusForbidden)
return
}
defer resp.Body.Close()
io.Copy(w, resp.Body)
}
The custom DialContext intercepts the destination IP right before the socket dial. It resolves DNS, tests with ip.IsPrivate() and ip.IsLoopback(), and connects directly to the validated IP. Setting CheckRedirect blocks redirect-based SSRF.
net.IP.IsPrivate() was added in Go 1.17. In older versions, manually check RFC 1918 CIDRs via net.ParseCIDR.
Attackers can map custom domains to 127.0.0.1 or use hex IP notations (0x7f000001) that bypass hostname string checks.
gosec -include=G107 ./...
http.Transport with DialContext IP validation!ip.IsPrivate() && !ip.IsLoopback()http.Client.CheckRedirectBy resolving the IP inside DialContext and immediately calling dialer.DialContext on that specific IP string, the connection is pinned to the validated address.