Skip to content
Web security Reviewed 2026-09-12

Open Redirect

What does this mean ?

An open redirect lets an untrusted party choose the destination of a redirect issued by a trusted application. Redirect targets can come from query parameters, form fields, headers or browser fragments. A destination that looks related to the current hostname is not necessarily trusted.

What can happen ?

A redirect can send visitors to an unintended site or interfere with an authentication flow. Protocol-specific callback validation matters for OAuth and OIDC. Restricting an ordinary application redirect is not a substitute for the identity provider's exact registered redirect-URI checks.

Recommendation

Prefer a short destination ID mapped to a fixed local route. If a feature genuinely needs arbitrary local return paths, use the framework's documented local-URL validator and reject network-path references such as //host, unexpected schemes and ambiguous parsing. Do not rely on substring matches.

Keep sensitive information out of redirect URLs. Do not implement login with hardcoded credentials merely to demonstrate a redirect. Use an appropriate redirect status for the flow; after a completed form POST, 303 commonly tells the browser to fetch the next page with GET.

Sample Code

These examples accept dashboard or settings; the request never supplies a URL. The local destinations are application-owned. Authentication and any completed form action occur separately.

def return_path(destination: str) -> str:
    routes = {"dashboard": "/dashboard/", "settings": "/settings/"}
    if destination not in routes:
        raise ValueError("Unknown destination")
    return routes[destination]

In a web handler, pass this result to the framework's redirect response. Do not pass request.GET['url'] directly.

const returnRoutes = new Map([
  ['dashboard', '/dashboard/'], ['settings', '/settings/']
]);
function returnPath(destination) {
  const path = returnRoutes.get(destination);
  if (!path) throw new Error('Unknown destination');
  return path;
}
// Express, after a completed form action:
// res.redirect(303, returnPath(req.body.destination));
string returnPath = destination switch
{
    "dashboard" => "/dashboard/",
    "settings" => "/settings/",
    _ => throw new ArgumentException("Unknown destination")
};
// In ASP.NET Core MVC, LocalRedirect(returnPath) adds a local-URL check.
String path = switch (destination) {
    case "dashboard" -> "/dashboard/";
    case "settings" -> "/settings/";
    default -> throw new IllegalArgumentException("Unknown destination");
};
response.setStatus(303);
response.setHeader("Location", path);

This Java 17+ servlet fragment runs before response content is committed.

$routes = ['dashboard' => '/dashboard/', 'settings' => '/settings/'];
if (!is_string($destination) || !array_key_exists($destination, $routes)) {
    throw new InvalidArgumentException('Unknown destination');
}
header('Location: ' . $routes[$destination], true, 303);
exit;
routes := map[string]string{"dashboard": "/dashboard/", "settings": "/settings/"}
path, ok := routes[destination]
if !ok {
    http.Error(w, "Unknown destination", http.StatusBadRequest)
    return
}
http.Redirect(w, r, path, http.StatusSeeOther)
routes = { 'dashboard' => '/dashboard/', 'settings' => '/settings/' }
path = routes.fetch(destination) { raise ArgumentError, 'Unknown destination' }
redirect_to path, status: :see_other

Rails has additional open-redirect protections; do not disable them simply to accept a user-supplied external URL.

Regression checks

Verify both destination IDs produce exactly the expected local routes. Reject an unknown ID, full external URL, network-path URL, backslash variant and empty input. After a form POST, check the actual response status and Location header. For authentication callbacks, test the identity provider's registered redirect policy separately.

References