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.open-redirect

Untrusted request data flows into an HTTP redirect destination.

CWE-601 OWASP A01:2021 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) through your code to an HTTP redirect destination, 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 redirect target (via a query parameter, form field, or request header) can forward the victim to an arbitrary external site while the link still appears to point at your trusted domain, a classic open redirect, commonly abused to bypass OAuth redirect_uri checks and to mount convincing phishing.

Do not pass request-derived values straight into http.Redirect(...) or a Location header. Validate the destination against an explicit allow-list, or restrict it to a known relative path (reject absolute URLs, scheme-relative //host values, and back-references) before redirecting. See CWE-601.

VULNERABLE
vulnerable.go
package main

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

// Inline: a query parameter flows straight into http.Redirect.
func redirectQuery(w http.ResponseWriter, r *http.Request) {
	// ruleid: auth.go.flow.open-redirect
	http.Redirect(w, r, r.URL.Query().Get("next"), http.StatusFound)
}

// Indirection: form value assigned to a local, then redirected.
func redirectForm(w http.ResponseWriter, r *http.Request) {
	dest := r.FormValue("url")
	// ruleid: auth.go.flow.open-redirect
	http.Redirect(w, r, dest, http.StatusFound)
}

// Header-derived destination written to the Location header directly.
func redirectHeader(w http.ResponseWriter, r *http.Request) {
	target := r.Header.Get("X-Forward-To")
	// ruleid: auth.go.flow.open-redirect
	w.Header().Set("Location", target)
	w.WriteHeader(http.StatusFound)
}

// PostFormValue source flowing into a Location header.
func redirectPostForm(w http.ResponseWriter, r *http.Request) {
	// ruleid: auth.go.flow.open-redirect
	w.Header().Set("Location", r.PostFormValue("return_to"))
	w.WriteHeader(http.StatusSeeOther)
}

// Parse-then-reflect without a host check: url.Parse does not validate
// anything, so the parsed destination is still attacker-controlled.
func redirectParsedUnchecked(w http.ResponseWriter, r *http.Request) {
	raw := r.URL.Query().Get("next")
	u, _ := url.Parse(raw)
	// ruleid: auth.go.flow.open-redirect
	http.Redirect(w, r, u.String(), http.StatusFound)
}

func main() {
	http.HandleFunc("/q", redirectQuery)
	http.HandleFunc("/pu", redirectParsedUnchecked)
	http.HandleFunc("/f", redirectForm)
	http.HandleFunc("/h", redirectHeader)
	http.HandleFunc("/p", redirectPostForm)
	_ = http.ListenAndServe(":8080", nil)
}
SAFE
safe.go
package main

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

// Allow-list of known-safe relative destinations.
var allowedRedirects = map[string]bool{
	"/home":      true,
	"/dashboard": true,
}

// Allow-list of hosts we are willing to redirect to.
var allowedRedirectHosts = map[string]bool{
	"app.example.com": true,
	"www.example.com": true,
}

// validateRedirect returns a vetted destination or a safe default.
func validateRedirect(dest string) string {
	if allowedRedirects[dest] {
		return dest
	}
	return "/home"
}

// Safe: redirect to a hard-coded constant path — no untrusted input.
func redirectConstant(w http.ResponseWriter, r *http.Request) {
	// ok: auth.go.flow.open-redirect
	http.Redirect(w, r, "/home", http.StatusFound)
}

// Safe: the request value is passed through an allow-list validator that
// clears the taint before it reaches the sink.
func redirectValidated(w http.ResponseWriter, r *http.Request) {
	dest := validateRedirect(r.URL.Query().Get("next"))
	// ok: auth.go.flow.open-redirect
	http.Redirect(w, r, dest, http.StatusFound)
}

// Safe: parse the destination, then guard the redirect on a host allow-list
// lookup of the parsed host. The redirect only runs once the host is vetted.
func redirectParsedChecked(w http.ResponseWriter, r *http.Request) {
	raw := r.URL.Query().Get("next")
	u, _ := url.Parse(raw)
	if allowedRedirectHosts[u.Host] {
		// ok: auth.go.flow.open-redirect
		http.Redirect(w, r, raw, http.StatusFound)
	}
}

func main() {
	http.HandleFunc("/c", redirectConstant)
	http.HandleFunc("/v", redirectValidated)
	http.HandleFunc("/pc", redirectParsedChecked)
	_ = 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.open-redirect -- <reason>

References

https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html ↗https://cwe.mitre.org/data/definitions/601.html ↗