# Open redirect vulnerability: examples, CVEs, safe code and test cases

Source: https://penetrationtestservices.com/vulnerabilities/open-redirect/
Publisher: Excession Labs (penetrationtestservices.com)
Last updated: 2026-11-09

What an open redirect is, how it chains into OAuth token theft and SSRF, test cases a naive check misses, four real CVEs, and safe redirect code for 7 stacks.

> **Short answer:** An open redirect (open redirection) is a flaw where an application sends the browser to a URL taken from user input without checking it. Attackers use it to make phishing links look trusted and to leak OAuth tokens. Fix it by redirecting only to local paths, an allowlist or a server-side ID map. On its own it is usually a low finding.

An open redirect (open redirection) is a flaw where an application sends the browser to a URL that comes from user input without checking where it points. Attackers use it to give a phishing link your trusted domain, and to leak OAuth codes and tokens. CWE-601 names the weakness. This page shows the vulnerable line, how attackers chain it, the inputs a naive check misses, four CVEs, safe code for seven stacks and the evidence a tester records.

Here is the vulnerable pattern in Express. It redirects to whatever the `next` parameter holds:

```js
app.get("/login/done", (req, res) => res.redirect(req.query.next));
```

## How does an open redirect work?

The application builds a redirect from a request value. The server sends a `302` response with a `Location` header, or client-side code sets `window.location`, and the browser goes where the value says. The link the victim sees starts with your domain, so it looks safe.

> **Illustrative request: Harbor Notes:** This is a demonstration. No real customer system was tested. Harbor Notes is a fictional note app and example.com is a reserved example domain. attacker.test is a reserved test domain.
> 
> A login flow at `https://app.example.com/login?next=/notes` sends the user to `/notes` after sign-in. A tester with written authorization changes `next` to an external host they control:
> 
> ```http
> GET /login/done?next=https://attacker.test/landing HTTP/1.1
> Host: app.example.com
> 
> HTTP/1.1 302 Found
> Location: https://attacker.test/landing
> ```
> 
> The `Location` header holds the external URL, so the application redirects off-site on request. The evidence for the report is the full request URL, the status and the `Location` value. The tester stops here. No further action against a real user is needed to prove the flaw.
Microsoft's ASP.NET Core documentation describes the standard login version: an attacker sends a link to your login page with a `returnUrl` that points to a look-alike site, the user signs in, and the app sends them to the look-alike, where a second login form collects the credentials. The user thinks the first sign-in failed.

## How do attackers abuse an open redirect?

Open redirects matter because of what they enable. The common chains are below.

| Chain | What the redirect gives the attacker | Source |
|---|---|---|
| Phishing | A link that starts with the real domain, so users and mail filters trust it | Microsoft Learn |
| OAuth code or token leak | A trusted redirect target that forwards the code or token to another host | RFC 9700 |
| SSRF filter bypass | A URL that passes a host check, then sends the server somewhere else | CVE-2024-22243 description |
| Client-side redirect to a script URL | A `javascript:` value assigned to `window.location` can run script (DOM-based) | PortSwigger, OWASP WSTG |

RFC 9700 (OAuth 2.0 Security Best Current Practice, January 2025) is direct about this. It says clients and authorization servers must not expose open redirectors, and warns that "Open redirectors can enable exfiltration of authorization codes and access tokens." It also describes an attack that combines a client-side open redirector with the way browsers reattach URL fragments, which can steal implicit-grant tokens even when the redirect URI pattern is narrow. It notes that strict URL matching stops this. See the [OAuth vulnerabilities page](/vulnerabilities/oauth-vulnerabilities/) for the wider flow.

## Which parameters and sinks should you check?

Check any place that takes a destination from the client. These are the usual parameter names: `next`, `url`, `returnUrl`, `return_to`, `redirect`, `redirect_uri`, `continue`, `dest` and `goto`. Check these places in the application too:

- Login, logout, registration and password reset flows that return the user to a page.
- SSO and OAuth callbacks, and the `redirect_uri` parameter.
- Link trackers, "leaving the site" pages and email click-through links.
- Language and region switchers.
- The `Host` and `Referer` headers, when the server builds a redirect from them. CVE-2021-22881 below is a `Host` header case.
- Client-side code that assigns `location`, `location.href`, `window.open` or `location.replace` from a URL value.

Server-side sinks appear as a `Location` header. Client-side sinks need a browser, because the redirect happens in JavaScript.

## Which inputs does a naive check miss?

A check that looks at the start or the text of the string fails on inputs the browser reads differently. The table shows what three common naive checks accept. These results come from reading the check logic against the WHATWG URL Standard, which says browsers treat a backslash as a slash in special URLs. They are not lab runs, so run each case on your own test application.

| Test input | `startsWith("/")` | `startsWith("https://app.example.com")` | Contains `app.example.com` | Where the browser goes |
|---|---|---|---|---|
| `//attacker.test/` | Accepts | Rejects | Rejects | `attacker.test` |
| `/\attacker.test/` | Accepts | Rejects | Rejects | `attacker.test` (backslash read as slash) |
| `https://app.example.com@attacker.test/` | Rejects | Accepts | Accepts | `attacker.test` (text before `@` is userinfo) |
| `https://app.example.com.attacker.test/` | Rejects | Accepts | Accepts | `attacker.test` |
| `https://attacker.test/?x=app.example.com` | Rejects | Rejects | Accepts | `attacker.test` |
| `javascript:` URL in a client-side sink | Rejects | Rejects | Accepts if the text matches | Runs script in the page |

Every row is accepted by at least one common check, so no one string test is enough. The OWASP cheat sheet says: "Do not use raw prefix or suffix matching." It also says to parse the URL with a maintained parser, reject userinfo and ambiguous input, and redirect to the URL you validated, without changing it afterward. Django's `url_has_allowed_host_and_scheme` follows the same idea and, per its source, checks backslashes, control characters and scheme-relative URLs.

## Which real CVEs show open redirects?

These four NVD entries are open redirects in widely used frameworks. Dates are the NVD publication dates, checked 9 November 2026.

| CVE | Published | Product | Root cause in one line |
|---|---|---|---|
| [CVE-2017-7233](https://nvd.nist.gov/vuln/detail/CVE-2017-7233) | 4 April 2017 | Django 1.8, 1.9, 1.10 before the fixed releases | `is_safe_url()` treated some numeric URLs as safe |
| [CVE-2018-14574](https://nvd.nist.gov/vuln/detail/CVE-2018-14574) | 3 August 2018 | Django 1.11 before 1.11.15, 2.0 before 2.0.8 | `CommonMiddleware` had an open redirect |
| [CVE-2021-22881](https://nvd.nist.gov/vuln/detail/CVE-2021-22881) | 11 February 2021 | Rails Action Pack before 6.1.2.1 and 6.0.3.5 | A crafted `Host` header and an allowed host with a leading dot made the Host Authorization middleware redirect to another site |
| [CVE-2024-22243](https://nvd.nist.gov/vuln/detail/CVE-2024-22243) | 23 February 2024 | Spring Framework | Apps that validated the host of a URL parsed by `UriComponentsBuilder` could be open to open redirect or SSRF |

Two lessons come from the list. The first two entries are in the redirect check itself, so a home-made check is likely to have the same kind of gap. The Rails and Spring entries show that the redirect target can come from a header or from a parser, as well as from a query parameter. Update the framework and also review your own redirect code.

## How do you fix an open redirect?

Prefer a design that never takes a full URL from the user. OWASP gives this order of choice.

1. Avoid redirects that take user input.
2. Map a short ID or name to the full URL on the server. OWASP calls this the highest protection, and warns that attackers can enumerate IDs, so do not map anything that must stay secret.
3. Accept only a local path, and use a framework helper.
4. If external targets are needed, parse the URL and check scheme, host and port against an explicit allowlist.
5. For links to other sites, show an interstitial page that names the destination and asks for a click.

The snippets below do option 3 or 4. They use fictional hosts.

**Node.js and Express.** The helper accepts only a path that starts with one slash, then checks that the parsed URL stays on the same origin:

```js
function safeNext(raw, fallback = "/") {
  if (typeof raw !== "string") return fallback;
  if (!/^\/(?![\/\\])[^\\\r\n\t]*$/.test(raw)) return fallback;
  const base = "https://app.example.com";
  return new URL(raw, base).origin === base ? raw : fallback;
}
app.get("/login/done", (req, res) => res.redirect(safeNext(req.query.next)));
```

**Python and Django.** This is the pattern from Django's source and release notes. The function name changed in Django 3.0 and the old name was removed in 4.0:

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

def after_login(request):
    next_url = request.GET.get("next")
    if url_has_allowed_host_and_scheme(
        url=next_url,
        allowed_hosts={request.get_host()},
        require_https=request.is_secure(),
    ):
        return redirect(next_url)
    return redirect("home")
```

**Python and Flask.** Flask has no helper. Normalize the backslash, then allow only a path:

```python
from urllib.parse import urlsplit
from flask import redirect, request

def is_local(target):
    t = (target or "").replace("\\", "/")
    parts = urlsplit(t)
    return t.startswith("/") and not t.startswith("//") and not parts.netloc and not parts.scheme

@app.get("/login/done")
def done():
    nxt = request.args.get("next", "")
    return redirect(nxt if is_local(nxt) else "/")
```

**Java and Spring.** Parse with `java.net.URI`, allow a local path or an allowlisted host, and reject userinfo. CVE-2024-22243 shows that host checks on a URL parsed by `UriComponentsBuilder` are not safe, so do not rely on that pattern.

```java
private static final Set ALLOWED_HOSTS = Set.of("app.example.com");

static String safeTarget(String raw) {
    try {
        URI u = new URI(raw);
        boolean local = u.getScheme() == null && u.getRawAuthority() == null
            && raw.startsWith("/") && !raw.startsWith("//") && !raw.contains("\\");
        boolean allowed = "https".equals(u.getScheme()) && u.getRawUserInfo() == null
            && u.getHost() != null && ALLOWED_HOSTS.contains(u.getHost());
        if (local || allowed) return raw;
    } catch (URISyntaxException e) { /* fall through */ }
    return "/";
}
// return "redirect:" + safeTarget(next);
```

**C# and ASP.NET Core.** Microsoft's documentation shows `LocalRedirect`, which throws for a non-local URL, and `Url.IsLocalUrl`:

```csharp
public IActionResult SomeAction(string redirectUrl)
{
    return LocalRedirect(redirectUrl);
}

private IActionResult RedirectToLocal(string returnUrl)
{
    if (Url.IsLocalUrl(returnUrl)) return Redirect(returnUrl);
    return RedirectToAction(nameof(HomeController.Index), "Home");
}
```

**Ruby on Rails.** The Rails API documentation says Rails protects against redirects to external hosts by default, and an app opts out with `allow_other_host: true`. Search the code for that option. `url_from` returns the URL only if it is internal, and matches the host exactly:

```ruby
redirect_to url_from(params[:redirect_url]) || root_url
```

**PHP and Laravel.** This snippet is ours and follows the OWASP rule to accept only a local path:

```php
$next = (string) $request->query('next', '/');
$local = str_starts_with($next, '/')
    && !str_starts_with($next, '//')
    && !str_contains($next, '\\');
return redirect($local ? $next : '/');
```

Redirect to the value you validated and do not change it after the check.

## What are the exact rules for OAuth redirect URIs?

The authorization server must compare redirect URIs by exact string match. RFC 9700 says authorization servers "MUST utilize exact string matching", and the only exception is the port of a `localhost` redirect for native apps. Patterns are harder to implement correctly. The RFC's example is a wildcard such as `https://*.somesite.example/*`, which can accept an attacker's domain, and even correct wildcard handling can be exploited through subdomain takeover. The RFC also says `http` redirect URIs are not allowed, except for native-app loopback redirects.

An exact match does not help if the registered page itself has an open redirect, because the code or token then goes to the open redirector first. Test each registered redirect URI page for open redirects too.

## How do penetration testers test and rate an open redirect?

Testers find the destination parameters, change them to an external test host, and record the redirect. The evidence standard for a finding is the full URL, the redirect response (status and `Location` header) and a statement of any chain. For a client-side sink, the proof is browser-observed: the page, the URL and the navigation in a real browser.

1. Map the flows that return a user to a page, and list every destination parameter.
2. Send the test inputs from the table above, one at a time, to a reserved test host. Read the status and `Location` header, for example with `curl -i`.
3. Check client-side code that sets `location` from the URL, and test the same inputs in a browser.
4. Check `Host` and `Referer` handling where the server builds a redirect.
5. Check each registered OAuth redirect URI page for an open redirect.
6. If a chain is plausible, show it on a test account and system you are authorized to test. An OAuth code or token reaching a host you control proves the chain.

A standalone open redirect is rated low. The rating rises with a proven chain: a token leak is rated by what the token allows, and an SSRF bypass by what the server can reach. Say in the report which one applies and attach its evidence.

## Testing checklist

- [ ] Every parameter that holds a destination is listed, including those in login, logout and SSO flows.
- [ ] Each one rejects `//attacker.test/`, `/\attacker.test/` and userinfo URLs such as `https://app.example.com@attacker.test/`.
- [ ] Host and scheme checks parse the URL. No raw prefix, suffix or substring match.
- [ ] Client-side code does not assign user input to `location` without a check.
- [ ] `Host` and `Referer` values are never used to build a redirect target without an allowlist.
- [ ] OAuth redirect URIs are matched exactly, and the registered pages have no open redirect.
- [ ] Framework and libraries are updated past the CVEs in the table above.
- [ ] Rails apps have no unreviewed `allow_other_host: true`.
- [ ] External links go through an interstitial or an ID map.

## When should you get a professional test?

Get one when your product signs users in with OAuth or SSO, sends email links that return users to the app, or runs on several domains. Chains are the hard part: a tester finds the redirect, then checks whether it leaks a code, bypasses a host filter or makes a phishing link. Our [web application penetration testing](/web-application-penetration-testing/) service covers redirect handling in login and OAuth flows, and each finding in the report includes the request, the response and the chain, with a fix for your stack.

## Questions

### Is an open redirect a vulnerability by itself?

Yes, it is CWE-601, but programs and testers often rate it low when it stands alone. The rating rises when it chains into something with real impact, such as leaking an OAuth code or token, bypassing a host check in an SSRF filter, or giving a phishing link a trusted domain.

### What is the difference between an open redirect and SSRF?

An open redirect sends the user's browser to another URL. SSRF makes your server send a request to another URL. They meet when server code follows redirects, or when a host validation flaw allows both. CVE-2024-22243 in Spring Framework is described as open redirect or SSRF.

### Which parameters should I test for open redirect?

Look for parameters that hold a destination: next, url, returnUrl, redirect, redirect_uri, continue, dest, return_to and goto. Also check login and logout flows, link trackers, language switchers and SSO callbacks. Read the Location header, and for client-side code read the JavaScript that sets window.location.

### Does a startsWith('/') check stop open redirects?

No. A value that starts with // is a protocol-relative URL, and a value that starts with /\ is treated as // by browsers that follow the WHATWG URL rules. Parse the URL, reject authority parts, and compare the result to an allowlist or use your framework's local-redirect helper.

### Are open redirects fixed by a redirect confirmation page?

An interstitial page that shows the destination and asks the user to click reduces phishing risk, and OWASP lists it as an option. It does not help OAuth token leaks, because the browser follows the redirect in the authorization flow without a user click. Use exact redirect URI matching there.

## Sources

- [OWASP Cheat Sheet Series: Unvalidated Redirects and Forwards](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html)
- [OWASP Web Security Testing Guide: Testing for Client-side URL Redirect](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/04-Testing_for_Client-side_URL_Redirect)
- [CWE-601: URL Redirection to Untrusted Site ('Open Redirect')](https://cwe.mitre.org/data/definitions/601.html)
- [PortSwigger Web Security Academy: DOM-based open redirection](https://portswigger.net/web-security/dom-based/open-redirection)
- [RFC 9700: Best Current Practice for OAuth 2.0 Security (January 2025)](https://www.rfc-editor.org/rfc/rfc9700.html)
- [Microsoft Learn: Prevent open redirect attacks in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/preventing-open-redirects)
- [Rails API: ActionController::Redirecting](https://api.rubyonrails.org/classes/ActionController/Redirecting.html)
- [Ruby on Rails Guides: Securing Rails applications](https://guides.rubyonrails.org/security.html)
- [Django documentation: django.utils reference](https://docs.djangoproject.com/en/5.2/ref/utils/)
- [WHATWG URL Standard](https://url.spec.whatwg.org/)
- [NVD: CVE-2017-7233 (Django is_safe_url)](https://nvd.nist.gov/vuln/detail/CVE-2017-7233)
- [NVD: CVE-2018-14574 (Django CommonMiddleware)](https://nvd.nist.gov/vuln/detail/CVE-2018-14574)
- [NVD: CVE-2021-22881 (Rails Host Authorization middleware)](https://nvd.nist.gov/vuln/detail/CVE-2021-22881)
- [NVD: CVE-2024-22243 (Spring Framework UriComponentsBuilder)](https://nvd.nist.gov/vuln/detail/CVE-2024-22243)
