VNX-GO-006: Go server-side request forgery
Reports outbound HTTP requests, reverse proxies, net/tls dials and websocket dials whose URL or address starts with a request value (query, form, path parameter header, or a decoded or unmarshalled JSON body field), directly, via url.Parse or a variable, or behind a prefix that does not pin the host. Fixed hosts, configured destinations, allowlisted values and guarded dialers are not reported; a HasPrefix test against a bare scheme is normalisation, not a check.
| Field | Value |
|---|---|
| Rule ID | VNX-GO-006 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Go |
| CWE | CWE-918 |
| CAPEC | CAPEC-664 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | ssrf web cloud |
| Source | internal/sast/rules/vnx-go-006.rego |
Overview
This rule detects Go HTTP handlers that pass r.FormValue() or r.URL.Query() values directly to http.Get, http.Post, or http.NewRequest without validating the destination. Server-side request forgery (SSRF) allows an attacker to make your server perform HTTP requests to destinations of their choosing, typically internal services that are not exposed to the internet. This maps to CWE-918: Server-Side Request Forgery (SSRF).
Why This Matters
In cloud environments, SSRF is a critical vulnerability class because virtually every major cloud platform exposes an instance metadata service at a well-known IP address (169.254.169.254 on AWS, GCP, and Azure). By making your server fetch that URL, an attacker can retrieve IAM credentials, account identifiers, and bootstrap tokens, granting them access to your entire cloud infrastructure without any other foothold. Beyond the metadata service, SSRF enables attackers to probe and interact with internal microservices, databases, and management interfaces that are firewalled from the public internet. It can also be used to bypass IP-based allowlists, as the requests originate from your trusted server.
What Gets Flagged
The rule fires when http.Get/Head/Post/PostForm, http.NewRequest(WithContext), a client’s Get/Post, resty R().Get, httputil.NewSingleHostReverseProxy or net.Dial/tls.Dial receives a destination that starts with a request value: a query, form, header or path parameter, or a field of a struct decoded from the request body. The value may be inline, parsed with url.Parse, held in a variable assigned a few lines up, or follow a prefix that leaves the host open ("http://"+v, fmt.Sprintf("http://%s/...", v)). It does not fire for a fixed scheme and host followed by a path ("https://api.github.com/repos/"+v), configured destinations, a value checked against an allowlist or host comparison, a client whose dialer rejects private addresses, or cache and database Get calls.
// FLAGGED: user-supplied URL fetched server-side
func fetchHandler(w http.ResponseWriter, r *http.Request) {
target := r.FormValue("url")
resp, err := http.Get(target)
// Attacker sends: url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
// Server fetches cloud credentials and returns them
if err == nil {
io.Copy(w, resp.Body)
}
}
// FLAGGED: user-supplied URL in http.NewRequest
func proxyHandler(w http.ResponseWriter, r *http.Request) {
dest := r.URL.Query().Get("dest")
req, _ := http.NewRequest("GET", dest, nil)
client := &http.Client{}
resp, _ := client.Do(req)
io.Copy(w, resp.Body)
}
Remediation
- Validate the destination URL against an allowlist of permitted hosts. Parse the URL and check that the host matches an explicit list of allowed external services. Reject anything else, including private IP ranges.
import (
"fmt"
"net"
"net/http"
"net/url"
)
var allowedHosts = map[string]bool{
"api.trusted-partner.com": true,
"cdn.example.com": true,
}
func isAllowedURL(rawURL string) error {
u, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid URL")
}
if u.Scheme != "https" {
return fmt.Errorf("only HTTPS is permitted")
}
host := u.Hostname()
if !allowedHosts[host] {
return fmt.Errorf("host %q is not in the allowlist", host)
}
return nil
}
// SAFE: allowlist validation before outbound request
func fetchHandler(w http.ResponseWriter, r *http.Request) {
target := r.FormValue("url")
if err := isAllowedURL(target); err != nil {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
resp, err := http.Get(target)
if err != nil {
http.Error(w, "upstream error", http.StatusBadGateway)
return
}
defer resp.Body.Close()
io.Copy(w, resp.Body)
}
- Block private and loopback IP ranges. After DNS resolution, verify that the resolved IP address is not in a private or reserved range (
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,::1). Use a customhttp.Transportwith a dial hook.
func safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, _ := net.SplitHostPort(addr)
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
for _, ip := range ips {
if isPrivateIP(ip.IP) {
return nil, fmt.Errorf("request to private IP %s is forbidden", ip.IP)
}
}
return (&net.Dialer{}).DialContext(ctx, network, net.JoinHostPort(host, port))
}
- Disable redirects or validate them. SSRF attacks sometimes chain redirects to reach internal resources. Configure your
http.Clientto disallow redirects or re-validate the destination on each redirect.
References
- CWE-918: Server-Side Request Forgery (SSRF)
- OWASP Server-Side Request Forgery Prevention Cheat Sheet
- Go net/http package documentation
- Go net/url package documentation
- AWS SSRF and IMDSv2 mitigation
- CAPEC-664: Server-Side Request Forgery
- MITRE ATT&CK T1190 – Exploit Public-Facing Application
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-go-006
// nosec vnx-go-006
Or record an ignore rule: vulnetix ignore add --rule vnx-go-006 --reason "...". See Suppressing findings.