Server-Side Request Forgery via Tainted URL Host in Go

High Risk ssrf
gossrfserver-side-request-forgeryhttpsecurity

What it is

Server-side request forgery (SSRF) vulnerabilities occur when applications allow user input to control the destination host for HTTP requests, enabling attackers to make the server contact arbitrary hosts and potentially access internal services.

package mainimport (    "fmt"    "io/ioutil"    "net/http"    "time")func webhookHandler(w http.ResponseWriter, r *http.Request) {    callbackURL := r.FormValue("callback_url")        // VULNERABLE: User controls entire URL including host    resp, err := http.Get(callbackURL)    if err != nil {        http.Error(w, "Callback failed", 500)        return    }    defer resp.Body.Close()        body, _ := ioutil.ReadAll(resp.Body)    w.Write(body)}func proxyHandler(w http.ResponseWriter, r *http.Request) {    targetHost := r.Header.Get("X-Target-Host")    path := r.URL.Query().Get("path")        // VULNERABLE: Building URL with user-controlled host    targetURL := fmt.Sprintf("http://%s%s", targetHost, path)        client := &http.Client{Timeout: 30 * time.Second}    resp, err := client.Get(targetURL)    if err != nil {        http.Error(w, "Proxy request failed", 500)        return    }    defer resp.Body.Close()        // Forward response    for key, values := range resp.Header {        for _, value := range values {            w.Header().Add(key, value)        }    }        body, _ := ioutil.ReadAll(resp.Body)    w.Write(body)}func fetchDataHandler(w http.ResponseWriter, r *http.Request) {    apiEndpoint := r.URL.Query().Get("endpoint")        // VULNERABLE: User can specify any endpoint URL    client := &http.Client{}    resp, err := client.Get(apiEndpoint)    if err != nil {        http.Error(w, "Failed to fetch data", 500)        return    }    defer resp.Body.Close()        data, _ := ioutil.ReadAll(resp.Body)        w.Header().Set("Content-Type", "application/json")    w.Write(data)}func imageProxyHandler(w http.ResponseWriter, r *http.Request) {    imageURL := r.URL.Query().Get("url")        // VULNERABLE: No validation of image URL host    resp, err := http.Get(imageURL)    if err != nil {        http.Error(w, "Image not found", 404)        return    }    defer resp.Body.Close()        w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))        imageData, _ := ioutil.ReadAll(resp.Body)    w.Write(imageData)}
package mainimport (    "errors"    "fmt"    "io/ioutil"    "net"    "net/http"    "net/url"    "strings"    "time")// Allowed hosts for callbacksvar allowedCallbackHosts = map[string]bool{    "api.partner1.com": true,    "webhook.partner2.com": true,    "callbacks.trustedservice.com": true,}// Blocked private/internal IP rangesvar blockedIPRanges = []string{    "10.0.0.0/8",    "172.16.0.0/12",    "192.168.0.0/16",    "127.0.0.0/8",    "169.254.0.0/16", // AWS metadata    "::1/128",    "fc00::/7",}func webhookHandler(w http.ResponseWriter, r *http.Request) {    callbackURL := r.FormValue("callback_url")        // SECURE: Validate URL before making request    if !isValidCallbackURL(callbackURL) {        http.Error(w, "Invalid callback URL", 400)        return    }        client := &http.Client{        Timeout: 10 * time.Second,        Transport: &http.Transport{            // Prevent following redirects to internal hosts            DisableCompression: true,        },    }        resp, err := client.Get(callbackURL)    if err != nil {        http.Error(w, "Callback failed", 500)        return    }    defer resp.Body.Close()        // Limit response size    limitedReader := &io.LimitedReader{R: resp.Body, N: 1024 * 1024} // 1MB limit    body, _ := ioutil.ReadAll(limitedReader)    w.Write(body)}func isValidCallbackURL(urlStr string) bool {    if urlStr == "" {        return false    }        parsedURL, err := url.Parse(urlStr)    if err != nil {        return false    }        // Only allow HTTPS    if parsedURL.Scheme != "https" {        return false    }        // Check host allowlist    if !allowedCallbackHosts[parsedURL.Host] {        return false    }        // Resolve and check IP address    return !isBlockedIP(parsedURL.Host)}func isBlockedIP(host string) bool {    // Extract hostname without port    hostname := host    if strings.Contains(host, ":") {        hostname = strings.Split(host, ":")[0]    }        // Resolve IP addresses    ips, err := net.LookupIP(hostname)    if err != nil {        return true // Block on DNS resolution failure    }        // Check each IP against blocked ranges    for _, ip := range ips {        for _, blockedRange := range blockedIPRanges {            _, subnet, err := net.ParseCIDR(blockedRange)            if err != nil {                continue            }            if subnet.Contains(ip) {                return true            }        }    }        return false}func proxyHandlerSecure(w http.ResponseWriter, r *http.Request) {    // SECURE: Use predefined service mapping instead of user host control    serviceName := r.Header.Get("X-Service")    path := r.URL.Query().Get("path")        serviceMap := map[string]string{        "api": "https://api.internal.com",        "data": "https://data.internal.com",        "reports": "https://reports.internal.com",    }        baseURL, exists := serviceMap[serviceName]    if !exists {        http.Error(w, "Invalid service", 400)        return    }        // Validate path    if !isValidPath(path) {        http.Error(w, "Invalid path", 400)        return    }        targetURL := baseURL + path        client := &http.Client{Timeout: 10 * time.Second}    resp, err := client.Get(targetURL)    if err != nil {        http.Error(w, "Service request failed", 500)        return    }    defer resp.Body.Close()        // Forward safe headers only    safeHeaders := []string{"Content-Type", "Cache-Control"}    for _, header := range safeHeaders {        if value := resp.Header.Get(header); value != "" {            w.Header().Set(header, value)        }    }        limitedReader := &io.LimitedReader{R: resp.Body, N: 10 * 1024 * 1024} // 10MB limit    body, _ := ioutil.ReadAll(limitedReader)    w.Write(body)}func isValidPath(path string) bool {    // Validate path doesn't contain traversal attempts    if strings.Contains(path, "..") {        return false    }        // Ensure path starts with /    if !strings.HasPrefix(path, "/") {        return false    }        // Additional path validation as needed    return true}func fetchDataHandlerSecure(w http.ResponseWriter, r *http.Request) {    // SECURE: Use predefined endpoint mapping    endpointName := r.URL.Query().Get("endpoint")        allowedEndpoints := map[string]string{        "weather": "https://api.weather.com/v1/current",        "news": "https://api.news.com/v1/headlines",        "stocks": "https://api.finance.com/v1/quotes",    }        endpointURL, exists := allowedEndpoints[endpointName]    if !exists {        http.Error(w, "Invalid endpoint", 400)        return    }        client := &http.Client{        Timeout: 15 * time.Second,        Transport: &http.Transport{            MaxResponseHeaderBytes: 4096,        },    }        resp, err := client.Get(endpointURL)    if err != nil {        http.Error(w, "Failed to fetch data", 500)        return    }    defer resp.Body.Close()        limitedReader := &io.LimitedReader{R: resp.Body, N: 5 * 1024 * 1024} // 5MB limit    data, _ := ioutil.ReadAll(limitedReader)        w.Header().Set("Content-Type", "application/json")    w.Write(data)}

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Building URLs with user input for the host or domain portion without validation, enabling SSRF attacks.

Root causes

User-Controlled URL Construction

Building URLs with user input for the host or domain portion without validation, enabling SSRF attacks.

Missing URL Parsing Validation

Not validating parsed URL components before making HTTP requests to prevent requests to internal services.

Redirect Following Without Checks

Following HTTP redirects without validating destination hosts, allowing attackers to redirect to internal networks.

Fixes

1

Implement URL Allowlist

Validate URL hosts against a strict allowlist of permitted domains before making requests.

2

Parse and Validate URLs

Use url.Parse() and validate scheme, host, and port before passing to http.Get() or http.Client.

3

Disable or Control Redirects

Set http.Client.CheckRedirect to validate redirect destinations and prevent SSRF through redirects.

Detect This Vulnerability in Your Code

Sourcery automatically identifies server-side request forgery via tainted url host in go and many other security issues in your codebase.