Skip to content
Open Redirect

Open Redirect: How Your Trusted Link Sends Users to an Attacker

An open redirect lets an attacker borrow your domain's credibility to bounce victims to phishing pages and steal OAuth tokens. Here is how the unvalidated redirect parameter is abused and how to constrain it.

Part 1 of 2From the Open Redirect series

On this page

An open redirect happens when an application takes a user-supplied URL and redirects the browser to it without checking that the destination is one it trusts. On its own it does not leak data or run code — which is exactly why it is so often dismissed. But it hands an attacker something valuable: the ability to launch an attack from your domain. The link starts on a site the victim trusts and quietly lands somewhere the attacker controls.

Those redirect, next and returnUrl parameters that pepper login flows and post-action pages are the usual culprits. This post shows how an unvalidated one becomes a phishing and token-theft tool, and how to lock the redirect down to destinations you actually own.

How it happens

The pattern shows up wherever an app needs to send you somewhere after an action — typically back to where you came from after logging in. The destination is read from the request and used verbatim:

public IActionResult Login(string returnUrl)
{
    // ... authenticate the user ...
    return Redirect(returnUrl);
}

The developer expects returnUrl to be something like /dashboard — a path on the same site. But the value is attacker-controlled, and Redirect() will happily send the browser to any URL it is handed, including an absolute one pointing at a completely different domain.

A concrete attack

The attacker crafts a link that begins on the trusted site but carries an off-site returnUrl:

https://app.example/login?returnUrl=https://app-example.evil.test/login

The victim sees app.example at the start of the URL, trusts it, and signs in. After authentication the app issues:

HTTP/1.1 302 Found
Location: https://app-example.evil.test/login

The browser follows it to a pixel-perfect clone of the login page on the attacker's domain, which harvests the credentials the user re-enters. Filters that only block obvious https:// prefixes are sidestepped with protocol-relative and malformed variants that browsers still treat as off-site:

returnUrl=//evil.test                 # protocol-relative, inherits https
returnUrl=/\evil.test                 # backslash that browsers normalize to //
returnUrl=https:evil.test             # missing-slash form
returnUrl=https://[email protected]   # userinfo trick; host is evil.test

The same primitive is even nastier in OAuth flows: if redirect_uri is not strictly validated, the authorization code or token meant for the real app is delivered straight to the attacker's domain.

The vulnerability is not that the app redirects. It is that the app lets the attacker decide the destination, while the victim sees only your trusted name at the start of the link.

What an attacker gains

A redirect the attacker controls is a credibility laundering machine:

  • Convincing phishing — the lure begins on your real domain, defeating "check the link" advice.
  • OAuth token and code theft — a loose redirect_uri hands authorization codes or access tokens to the attacker.
  • Malware delivery — bouncing users to drive-by download or exploit pages under cover of your brand.
  • Filter and reputation bypass — security tools that trust your domain wave the initial link through.

The fix: allowlist destinations, prefer relative paths

The reliable rule is to never redirect to a raw user-supplied URL. Constrain the destination to things you control. The simplest robust approach is to only allow same-site relative paths and reject anything absolute or protocol-relative:

public IActionResult Login(string returnUrl)
{
    // ... authenticate the user ...

    if (!Url.IsLocalUrl(returnUrl))
        returnUrl = "/dashboard";

    return Redirect(returnUrl);
}

Url.IsLocalUrl accepts /dashboard but rejects https://evil.test, //evil.test and the backslash and userinfo tricks above. When you genuinely must redirect off-site, validate the full hostname against an explicit allowlist of approved domains rather than pattern-matching the string — and for OAuth, require redirect_uri to exactly match a pre-registered value. Mapping a short token to a known-good URL server-side (?next=billing -> /account/billing) avoids exposing any redirect target at all.

Module: Open Redirect. How it probes: Absolute, protocol-relative, encoded and path-confusion targets in every redirect parameter the crawl observes. How it confirms: The final Location header or the client-side navigation has to land on the attacker-controlled host. A finding moves from Possible to Confirmed only after this check. Reported as CWE-601 · ATT&CK T1204.001 · SARIF 2.1.0.
How SelfSec proves this one
Module
Open Redirect
How it probes
Absolute, protocol-relative, encoded and path-confusion targets in every redirect parameter the crawl observes.
How it confirms
The final Location header or the client-side navigation has to land on the attacker-controlled host.
Reported as CWE-601 · ATT&CK T1204.001 · SARIF 2.1.0

A reflected response alone is not enough to promote a Open Redirect finding. The classical engine has to confirm the behavior before it reaches the report. See the detection engine

SelfSec scanner

Find Open Redirect in your own app

Reading about the class is one thing. SelfSec tests for it against an application you are authorized to assess, and only calls it real when independent evidence agrees.

Find it Confirmed, not guessed 21 vulnerability modules, with the classical engine owning the verdict. Prove it Evidence you can re-read Every finding keeps the raw request, the response and the baseline it was measured against.
Join the launch list