Description
An open redirect occurs when untrusted input determines an HTTP redirect destination without adequate validation. Attackers can send users to phishing, malware, or other unwanted sites through a trusted application's redirect. A client-controlled parameter, Host header, or URL component becomes dangerous when the application uses it to select an external destination in http.Redirect.
Potential impact
- Passing through a trusted domain can make phishing links more convincing.
- Redirects to malicious sites can expose users to malware or browser exploits.
- Abuse of the redirect can damage trust in the original service.
Remediation
- Validate the destination before passing it to
http.Redirect, especially its scheme (httporhttps), host, and port. - If external redirects are necessary, permit only approved domains from an allow-list.
- Prefer relative paths such as
/path/...; reject absolute URLs beginning withhttp://orhttps://, and network-path references such as//host. - Do not build a destination directly from
req.Host, which the client can control. Use a server-configured host. - Parse and resolve URLs with
url.ParseorResolveReference, then check that the final host and scheme are expected.
Examples
Before
go
package main
import (
"net/http"
)
// Before: use client-controlled Host and URL path directly
func redirectHandler(w http.ResponseWriter, r *http.Request) {
// The client can control r.Host and r.URL.Path
target := "https://" + r.Host + r.URL.Path
if len(r.URL.RawQuery) > 0 {
target += "?" + r.URL.RawQuery
}
// Redirect directly to the input-derived URL: open redirect
http.Redirect(w, r, target, http.StatusTemporaryRedirect)
}
func main() {
http.HandleFunc("/go", redirectHandler)
http.ListenAndServe(":8080", nil)
}
After
go
package main
import (
"net/http"
"net/url"
"strings"
)
var allowedHosts = map[string]bool{
"example.com": true,
"www.example.com": true,
}
// After: fixed domain with only relative paths accepted
func safeRedirectHandler(w http.ResponseWriter, r *http.Request) {
// 1) Always use a host fixed by the server
const baseURL = "https://www.example.com"
// 2) Read the destination path from a parameter
// Example: /go?next=/products/123
next := r.URL.Query().Get("next")
if next == "" {
next = "/" // Default path
}
// 3) Reject these absolute and protocol-relative URL prefixes
if strings.HasPrefix(next, "http://") ||
strings.HasPrefix(next, "https://") ||
strings.HasPrefix(next, "//") {
http.Error(w, "invalid redirect url", http.StatusBadRequest)
return
}
// 4) Ensure the path starts with '/'
if !strings.HasPrefix(next, "/") {
next = "/" + next
}
// Build the final redirect URL
u, err := url.Parse(baseURL)
if err != nil {
http.Error(w, "server error", http.StatusInternalServerError)
return
}
u.Path = next
// 5) Check the final host against the allow-list as an extra safeguard
if !allowedHosts[u.Host] {
http.Error(w, "invalid redirect host", http.StatusBadRequest)
return
}
http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
}
func main() {
http.HandleFunc("/go", safeRedirectHandler)
http.ListenAndServe(":8080", nil)
}
Explanation:
- Before: The code concatenates client-controlled
r.Host,r.URL.Path, andr.URL.RawQueryinto a redirect URL. - Setting the
Hostheader toevil.commakes that external host the destination in this example. - The server passes the result directly to
http.Redirectwithout validating the destination. - A user who trusts the application can therefore be redirected to a phishing or malware site.
- After: The example controls the destination as follows.
- A fixed server-side
baseURLreplaces trust inr.Host. - User control is limited to the path,
next, with a leading/ensured. - Inputs beginning with
http://,https://, or//are rejected. Accepted input is assigned tourl.URL.Path, so it remains a path on the fixed host. - The final host is checked against the allow-list as an additional safeguard.
Limiting what the user controls prevents this redirect from selecting an arbitrary external site.