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.