Open redirect

Open redirect

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 (http or https), 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 with http:// or https://, 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.Parse or ResolveReference, 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, and r.URL.RawQuery into a redirect URL.
  • Setting the Host header to evil.com makes that external host the destination in this example.
  • The server passes the result directly to http.Redirect without 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 baseURL replaces trust in r.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 to url.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.

References