Input Validation and SSRF
Input Validation and SSRF
Overview
Validate at trust boundaries. SSRF (server-side request forgery) happens when user-controlled URLs make your server fetch internal resources (169.254.169.254, localhost, cloud metadata).
Validation basics
type CreateBook struct {
Title string `json:"title"`
Price int `json:"price_cents"`
}
func (c CreateBook) Valid() error {
if len(c.Title) == 0 || len(c.Title) > 200 {
return fmt.Errorf("title length")
}
if c.Price < 0 {
return fmt.Errorf("price")
}
return nil
}- Prefer allowlists over blocklists for enums
- Bound string lengths and collection sizes
- Parse numbers with bit size (
ParseInt(..., 10, 64))
Path traversal
// never: os.Open("/data/" + userPath)
// use safeJoin from filepath chapterSSRF defenses
When your app fetches a URL on behalf of a user:
- Allowlist schemes (
httpsonly)
- Allowlist hosts or resolve and block private ranges
- No redirects to off-allowlist targets
- Timeouts + size limits
func safeURL(raw string) (*url.URL, error) {
u, err := url.Parse(raw)
if err != nil {
return nil, err
}
if u.Scheme != "https" {
return nil, fmt.Errorf("scheme")
}
host := u.Hostname()
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
// reject loopback, link-local, private RFC1918, metadata IP
return u, nil
}func isBadIP(ip net.IP) bool {
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() || ip.Equal(net.ParseIP("169.254.169.254"))
}Re-check IPs after redirects (pin or disable redirects).
Header injection
Don’t put raw user strings into Header.Set without validation (CRLF). Prefer structured values.
Rules of thumb
| Do | Don’t |
|---|---|
| Validate then use | Trust client JSON shapes unbounded |
| Allowlist outbound hosts | Open proxy to any URL |
| Bound body size | Stream forever into memory |
Try next
- Reject
http://127.0.0.1/in a fetch helper.
- Reject path
../../etc/passwdin download handler.
- Fuzz validators with
go test -fuzz.