An open redirect is any page on your site that sends the visitor's browser to a URL taken from user input without checking it. It looks minor. In practice it lets an attacker send a phishing link that shows your real domain, and in an OAuth setup it can leak authorization codes and access tokens.
Why a redirect is more than a cosmetic bug
CWE-601 describes the weakness as a web application accepting user-controlled input that specifies a link to an external site, then using that link in a redirect. The victim follows a link to your trusted site and lands on a page the attacker controls. The OWASP cheat sheet gives the shape:
http://example.com/example.php?url=http://malicious.example.com
The server name in the modified link is identical to the original site, so the phishing attempt looks more trustworthy. For the user's side of this, see our link checker.
With OAuth the same flaw reaches further. RFC 9700 says clients and authorization servers MUST NOT expose URLs that forward the browser to arbitrary URIs taken from a query parameter, because open redirectors can enable exfiltration of authorization codes and access tokens.
Forwards are a related case. If user input chooses an internal destination, as in function.jsp?fwd=admin.jsp, a crafted URL can pass the access control check and reach an administrative function. The application must check that the user is authorised for the target.
Finding the parameters
Names vary: url, fwd and returnUrl appear in the OWASP examples, and your code will have others. Look for the pattern, not the name.
| Stack | Dangerous pattern |
|---|---|
| Java | response.sendRedirect(request.getParameter("url")) |
| PHP | header("Location: " . $redirect_url) with $_GET['url'] |
| C# .NET | Response.Redirect(url) with request.QueryString["url"] |
| Rails | redirect_to params[:url] |
| Java forward | getRequestDispatcher(fwd).forward(...) |
A first pass with grep gives you a list to read. Adjust the path and file types to your repository.
grep -rnE 'sendRedirect\(|Redirect\(|redirect_to params|header::LOCATION' src/
grep -rn -A3 'header(.Location' src/
The second command is for PHP. Read the lines after each hit: without an exit after header("Location..., the rest of the script keeps running. A browser set to ignore the redirect may then show the rest of the page.
.NET teams should look at login actions first. The cheat sheet says ASP.NET MVC 1 and 2 sites are particularly vulnerable, and its MVC 2 LogOn example redirects to returnUrl with no validation. Its fix is to move to MVC 3.
Most hits will be harmless. Any redirect whose argument is not a string literal or a server-side lookup gets one question: where did the value come from? Cookies, hidden form fields, headers and database content all count. Many open redirects exist because the developer assumed a cookie or hidden field could not be modified.
To confirm a finding, do not stop at one plain external address. Send each of these in the parameter and watch the Location header:
http://malicious.example.comhttps://yoursite.example@malicious.example(userinfo:yoursite.examplebecomes a username)https://yoursite.example.malicious.example(a hostname that begins with yours)https://malicious-yoursite.example(a hostname that ends with yours)
The cheat sheet names userinfo and misleading hostnames as ways around prefix and suffix checks. If one of them passes where the plain URL was blocked, the check is probably string matching. That is an inference, not proof, but replace the check either way.
Scanners will catch some of these. The OWASP Web Security Testing Guide says such tools are generic and lack application-specific context, so verify each finding by hand.
Fixing them, in order of preference
- Do not redirect on user input. If the destination is fixed in code, an attacker cannot change it.
- Map IDs to targets server-side. The user supplies a short name or ID and the server looks up the full URL, rejecting everything else. Make sure the IDs cannot be cycled through, or a visitor can enumerate every target.
- Use the framework helper for local returns. In ASP.NET Core that means
LocalRedirectorIsLocalUrl, which reject non-local destinations. - Allowlist external destinations. Use a maintained URL parser and compare the parsed scheme, canonical host and effective port against an explicit list. Constrain the path if only specific endpoints are allowed.
- Optionally, require a nonce. CWE describes an unpredictable, application-generated nonce on every redirect request, with requests lacking it refused. XSS can bypass it, so it complements a fix and does not replace one.
Rules for the allowlist check:
- Reject userinfo and ambiguous input.
- Redirect using the validated URL, not a version you transform afterwards.
- Never use raw prefix or suffix matching. The cheat sheet cites GitLab as a case where userinfo and misleading hostnames defeated such checks.
A denylist of bad values is a weak primary defence, though CWE accepts it for detecting attacks. Use "accept known good" instead.
If people must leave your site, an interstitial page that names the destination and requires a click adds a further layer. Avoid XSS when building that page.
The OAuth side
RFC 9700 requires authorization servers to compare client redirection URIs against the pre-registered ones with exact string matching, except for port numbers in localhost redirection URIs of native apps. My reading, not RFC text: exact matching proves the callback is the registered one, so if that registered host also exposes an open redirector, the code can travel onwards. The RFC points to its Section 4.11 for the detail. Read it before relying on my summary.
Where to start
- Run the grep patterns above over your code and list every redirect and forward whose target comes from a request, cookie or stored value. Check login and OAuth callback handlers first.
- Check PHP hits for a missing
exit, and .NET projects for MVC 1 or 2 login actions. - Test each hit with the four inputs above, including the userinfo form, and record which ones send the browser off your domain.
- Replace user-supplied URLs with server-side ID mapping where you can, local-only framework helpers for return addresses, and a parsed-URL allowlist for the few external destinations you need.
- Review your OAuth client registrations for exact redirect URIs and confirm none of your hosts exposes an open redirector.
- Add a redirect test to code review and to your penetration testing scope, so the bug does not return.
