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.

SelfSec Team3 min read
Part 1 of 2from theOpen Redirectseries
returnUrl=https://evil.test
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.

How SelfSec proves this one

Scanner behaviour
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 behaviour before it reaches the report. See the detection engine

SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.

Do both things about Open Redirect

SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.

The Open Redirect series

  1. 01OverviewOpen Redirect: How Your Trusted Link Sends Users to an AttackerReading
  2. 02OAuth redirect_uriOAuth redirect_uri Hijacking: Stealing Tokens Through a Loose Redirect4 min

Related reading