RSTR-SSRF-004 — Go http.Get / http.NewRequest with request input

Summary

Go equivalent of RSTR-SSRF-001. A handler issues an outbound HTTP call where the URL is read from the request via r.FormValue / r.URL.Query().Get / mux.Vars(r).

Severity

High.

Languages

Go.

What rastray flags

http.Get, http.Head, http.Post, http.PostForm, http.NewRequest, and http.NewRequestWithContext when the URL argument is fed directly by r.FormValue(...), r.URL.Query().Get(...), or similar request accessors:

func proxy(w http.ResponseWriter, r *http.Request) {
    resp, err := http.Get(r.URL.Query().Get("url"))   // ← flagged
    ...
}
req, _ := http.NewRequest("POST",
    r.FormValue("target"), body)                       // ← flagged

What rastray deliberately does not flag

  • Literal URLs.
  • A URL stored in an intermediate variable.
  • URLs built via url.URL{Scheme:"https", Host: ALLOWED_HOST, Path: ...} where only the path comes from the request.

How to fix it

Allow-list the destination host and block private/metadata IPs:

var allowed = map[string]struct{}{
    "api.example.com": {},
    "cdn.example.com": {},
}

func safeGet(raw string) (*http.Response, error) {
    u, err := url.Parse(raw)
    if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
        return nil, errors.New("bad url")
    }
    if _, ok := allowed[u.Hostname()]; !ok {
        return nil, errors.New("host not allowed")
    }
    addrs, err := net.LookupIP(u.Hostname())
    if err != nil {
        return nil, err
    }
    for _, ip := range addrs {
        if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {
            return nil, errors.New("private destination")
        }
    }
    client := &http.Client{
        Timeout: 5 * time.Second,
        CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
            return http.ErrUseLastResponse  // do not follow
        },
    }
    return client.Get(raw)
}

References