VNX-GO-025: Potential open redirect via HTTP redirect
Detects a redirect (http.Redirect, gin/echo/fiber Redirect, a Location header) whose target is a query, form or route parameter, directly or through one variable built from it, with no allowlist or host check first.
| Field | Value |
|---|---|
| Rule ID | VNX-GO-025 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Go |
| CWE | CWE-601 |
| CAPEC | CAPEC-610 |
| ATT&CK | T1046 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | open-redirect http redirect |
| Source | internal/sast/rules/vnx-go-025.rego |
Overview
This rule flags instances where HTTP redirect functions (like http.Redirect) use user-controlled input to determine the redirect destination without proper validation. This pattern can lead to open redirect vulnerabilities where attackers can redirect users to malicious websites.
This maps to CWE-601: URL Redirection to Untrusted Site (‘Open Redirect’).
Why This Matters
Open redirect vulnerabilities occur when an application accepts user-controlled input to determine the destination of a redirect without proper validation. Attackers can craft URLs that appear to link to a trusted site but actually redirect victims to malicious sites for phishing, malware distribution, or credential theft.
These vulnerabilities are often overlooked but can be highly effective in social engineering attacks because the initial URL appears legitimate.
What Gets Flagged
A redirect whose target the requester chooses: a query, form or route parameter that reaches http.Redirect, a framework Redirect, or a Location header, directly or through one variable built from it.
// FLAGGED: ?next= is sent straight back as the redirect target
next := r.URL.Query().Get("next")
http.Redirect(w, r, next, http.StatusFound)
// FLAGGED: gin, with the value inline
c.Redirect(http.StatusFound, c.Query("return_to"))
// FLAGGED: a host built from the parameter
host := r.URL.Query().Get("tenant")
u := fmt.Sprintf("https://%s/home", host)
http.Redirect(w, r, u, http.StatusFound)
// FLAGGED: a Location header
w.Header().Set("Location", r.URL.Query().Get("RelayState"))
Not flagged: a target checked first (strings.HasPrefix, a map or switch allowlist, url.Parse followed by a Host/IsAbs test, a helper named isSafeRedirect), a value placed after a fixed path ("/users/" + id) or a fixed https://host/, values passed through url.QueryEscape, constant targets, and the Referer/Host headers.
Remediation
-
Validate redirect URLs against an allowlist: Only allow redirects to trusted domains or paths:
// SAFE: Validate against allowlist func loginHandler(w http.ResponseWriter, r *http.Request) { redirectURL := r.FormValue("redirect") if isAllowedRedirectURL(redirectURL) { http.Redirect(w, r, redirectURL, http.StatusFound) return } // Default to safe location http.Redirect(w, r, "/", http.StatusFound) } func isAllowedRedirectURL(url string) bool { // Parse URL and check host against allowed domains u, err := url.Parse(url) if err != nil { return false } allowed := map[string]bool{ "example.com": true, "www.example.com": true, } return allowed[u.Host] && u.IsAbs() // Only absolute URLs to allowed domains } -
Use relative paths for internal redirects: When redirecting within your own site:
// SAFE: Use relative paths that you control func handler(w http.ResponseWriter, r *http.Request) { // Validate it's a relative path without dangerous elements redirectPath := r.FormValue("redirect") if strings.HasPrefix(redirectPath, "/") && !strings.Contains(redirectPath, "..") && !strings.Contains(redirectPath, "//") { http.Redirect(w, r, redirectPath, http.StatusFound) return } http.Error(w, "Invalid redirect path", http.StatusBadRequest) } -
Implement redirect validation middleware: Create reusable validation logic:
// SAFE: Middleware for redirect validation func redirectValidator(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/redirect" { target := r.URL.Query().Get("url") if !isSafeRedirectTarget(target) { http.Error(w, "Invalid redirect URL", http.StatusBadRequest) return } } next.ServeHTTP(w, r) }) } -
Use URL parsing and validation: Always parse and validate URLs properly:
// SAFE: Proper URL validation func isSafeRedirectTarget(input string) bool { if input == "" { return false } // Prevent protocol-relative URLs that could be dangerous if strings.HasPrefix(input, "//") { return false } u, err := url.Parse(input) if err != nil { return false } // Allow only relative paths or known safe absolute URLs if !u.IsAbs() { // Relative path - additional validation return strings.HasPrefix(u.Path, "/") && !strings.Contains(u.Path, "..") } // Absolute URL - check against allowlist allowedHosts := map[string]bool{ "example.com": true, "trusted-partner.com": true, } return allowedHosts[u.Hostname()] }
References
- CWE-601: URL Redirection to Untrusted Site (‘Open Redirect’)
- OWASP Application Security Verification Standard v4.0 – V4.1.2 Redirect Validation
- OWASP Open Redirect Cheat Sheet
- Go net/http package documentation
- Go net/url package documentation
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-025
// nosec vnx-go-025
Or record an ignore rule: vulnetix ignore add --rule vnx-go-025 --reason "...". See Suppressing findings.