v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
HIGH AI PREVALENCE: HIGH auth.go.flow.ssrf

Untrusted request data flows into the URL of an outbound HTTP request.

CWE-918 OWASP API7:2023 go DATAFLOW

Why AI tools produce this: AI coding tools generate this anti-pattern by default, it appears in a large share of AI-written auth code.

Dataflow rule. This is a taint-mode rule: it traces untrusted request input (query, body, params, cookies, headers) through your code to the URL of an outbound HTTP request, so indirection across multiple lines is caught, not just the direct one-line form. Routing the value through a recognised validation / allow-list sanitizer clears the taint and suppresses the finding. Why dataflow →

Why this matters

An attacker who controls the request target (via a query parameter, form field, or request header) can coerce your server into making requests to arbitrary destinations, Server-Side Request Forgery (SSRF). This is routinely abused to reach internal-only services behind your network perimeter and, most damagingly, the cloud instance metadata endpoint (e.g. http://169.254.169.254/...), letting an attacker steal short-lived credentials and pivot into your cloud account.

Never pass a request-derived value straight into http.Get, http.Post, http.NewRequest, or a client's Get/Post. Validate the destination host against an explicit allow-list (parse the URL and check the resolved host/scheme), and reject requests to private, loopback, and link-local address ranges before dialing. See CWE-918.

VULNERABLE
vulnerable.go
package main

import (
	"net/http"
	"net/url"
)

// Inline: a query parameter flows straight into http.Get.
func fetchQuery(w http.ResponseWriter, r *http.Request) {
	// ruleid: auth.go.flow.ssrf
	resp, _ := http.Get(r.URL.Query().Get("url"))
	defer resp.Body.Close()
}

// Indirection: form value assigned to a local, then requested.
func fetchForm(w http.ResponseWriter, r *http.Request) {
	target := r.FormValue("endpoint")
	// ruleid: auth.go.flow.ssrf
	resp, _ := http.Get(target)
	defer resp.Body.Close()
}

// http.Post with an untrusted URL argument.
func postForm(w http.ResponseWriter, r *http.Request) {
	dest := r.PostFormValue("callback")
	// ruleid: auth.go.flow.ssrf
	resp, _ := http.Post(dest, "application/json", nil)
	defer resp.Body.Close()
}

// http.Head against a header-derived URL.
func headHeader(w http.ResponseWriter, r *http.Request) {
	loc := r.Header.Get("X-Fetch-From")
	// ruleid: auth.go.flow.ssrf
	resp, _ := http.Head(loc)
	defer resp.Body.Close()
}

// http.NewRequest with the URL coming from a query parameter.
func buildRequest(w http.ResponseWriter, r *http.Request) {
	u := r.URL.Query().Get("target")
	// ruleid: auth.go.flow.ssrf
	req, _ := http.NewRequest("GET", u, nil)
	_ = req
}

// http.NewRequestWithContext with an untrusted URL.
func buildRequestCtx(w http.ResponseWriter, r *http.Request) {
	u := r.FormValue("uri")
	// ruleid: auth.go.flow.ssrf
	req, _ := http.NewRequestWithContext(r.Context(), "POST", u, nil)
	_ = req
}

// Custom client Get with a tainted URL.
func clientGet(w http.ResponseWriter, r *http.Request) {
	client := &http.Client{}
	u := r.URL.Query().Get("addr")
	// ruleid: auth.go.flow.ssrf
	resp, _ := client.Get(u)
	defer resp.Body.Close()
}

// Custom client Post with a tainted URL.
func clientPost(w http.ResponseWriter, r *http.Request) {
	client := &http.Client{}
	u := r.Header.Get("X-Upstream")
	// ruleid: auth.go.flow.ssrf
	resp, _ := client.Post(u, "text/plain", nil)
	defer resp.Body.Close()
}

// Parse-then-use without a host check: url.Parse does not validate anything,
// so the parsed result is still attacker-controlled — a real SSRF.
func fetchParsedUnchecked(w http.ResponseWriter, r *http.Request) {
	raw := r.URL.Query().Get("url")
	u, _ := url.Parse(raw)
	// ruleid: auth.go.flow.ssrf
	resp, _ := http.Get(u.String())
	defer resp.Body.Close()
}

func main() {
	http.HandleFunc("/q", fetchQuery)
	http.HandleFunc("/pu", fetchParsedUnchecked)
	http.HandleFunc("/f", fetchForm)
	http.HandleFunc("/p", postForm)
	http.HandleFunc("/h", headHeader)
	http.HandleFunc("/nr", buildRequest)
	http.HandleFunc("/nrc", buildRequestCtx)
	http.HandleFunc("/cg", clientGet)
	http.HandleFunc("/cp", clientPost)
	_ = http.ListenAndServe(":8080", nil)
}
SAFE
safe.go
package main

import (
	"net/http"
	"net/url"
	"strings"
)

// Allow-list of hosts this service is permitted to call.
var allowedHosts = map[string]bool{
	"api.internal.example.com": true,
	"images.example.com":       true,
}

// isAllowedHost vets a raw URL string against the allow-list, returning the
// vetted URL or a safe default, clearing the taint.
func isAllowedHost(raw string) string {
	u, err := url.Parse(raw)
	if err != nil || !allowedHosts[strings.ToLower(u.Host)] {
		return "https://api.internal.example.com/health"
	}
	return raw
}

// Safe: request a hard-coded constant URL — no untrusted input.
func fetchConstant(w http.ResponseWriter, r *http.Request) {
	// ok: auth.go.flow.ssrf
	resp, _ := http.Get("https://api.internal.example.com/health")
	defer resp.Body.Close()
}

// Safe: the request value is passed through a host allow-list validator that
// clears the taint before it reaches the sink.
func fetchValidated(w http.ResponseWriter, r *http.Request) {
	target := isAllowedHost(r.URL.Query().Get("url"))
	// ok: auth.go.flow.ssrf
	resp, _ := http.Get(target)
	defer resp.Body.Close()
}

// Safe: parse the URL, then guard the request on a host allow-list lookup of
// the parsed host. The request only runs once the host is vetted.
func fetchParsedChecked(w http.ResponseWriter, r *http.Request) {
	raw := r.URL.Query().Get("url")
	u, _ := url.Parse(raw)
	if allowedHosts[u.Host] {
		// ok: auth.go.flow.ssrf
		resp, _ := http.Get(raw)
		defer resp.Body.Close()
	}
}

func main() {
	http.HandleFunc("/c", fetchConstant)
	http.HandleFunc("/v", fetchValidated)
	http.HandleFunc("/pc", fetchParsedChecked)
	_ = http.ListenAndServe(":8080", nil)
}

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

// oauthlint-disable-next-line auth.go.flow.ssrf -- <reason>

References

https://owasp.org/www-community/attacks/Server_Side_Request_Forgery ↗https://cwe.mitre.org/data/definitions/918.html ↗