User-Controlled URL Construction
Building URLs with user input for the host or domain portion without validation, enabling SSRF attacks.
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)}The vulnerable code was updated to address the security issue.
Building URLs with user input for the host or domain portion without validation, enabling SSRF attacks.
Sourcery automatically identifies server-side request forgery via tainted url host in go and many other security issues in your codebase.