URL Redirection to Untrusted Site ('Open Redirect')
Description
URL Redirection to Untrusted Site occurs when a web application accepts a user-controlled input that specifies a link to an external site and uses that link to redirect the user. This allows attackers to craft URLs that appear to point to the legitimate site but redirect victims to malicious sites. Open redirects are commonly exploited in phishing attacks, as victims see the trusted domain in the URL before clicking. The vulnerability occurs in login pages, logout handlers, and any functionality that redirects based on user input.
Risk
Open redirects are a significant phishing enabler. Attackers craft URLs like https://trusted-bank.com/redirect?url=https://evil-site.com that victims trust because they see the legitimate domain. After clicking, victims land on attacker-controlled sites mimicking login pages. Open redirects can also be chained with other vulnerabilities—they can bypass SSRF protections, steal OAuth tokens, or facilitate XSS through javascript: URLs. While often rated as "low severity," their role in phishing campaigns makes them impactful.
Solution
Avoid user-controlled redirects when possible. If redirects are necessary, use a whitelist of allowed destinations. Validate redirect URLs against a list of trusted domains. Use indirect references (mapping IDs to URLs server-side) instead of direct URLs. Block javascript:, data:, and other dangerous URL schemes. For relative redirects, ensure the path doesn't contain protocol handlers. Display interstitial warnings before redirecting to external sites.
Common Consequences
| Impact | Details |
|---|---|
| Reputation | Scope: Phishing Enablement Trusted domains used in phishing attacks damage organizational reputation. |
| Confidentiality | Scope: Credential Theft Users redirected to fake login pages may disclose credentials. |
| Access Control | Scope: Token Theft OAuth and SSO flows can leak tokens through open redirects. |
Example Code + Solution Code
Vulnerable Code
# VULNERABLE: Direct redirect from parameter
from flask import Flask, request, redirect
@app.route('/redirect')
def redirect_handler():
url = request.args.get('url')
# Attacker: /redirect?url=https://evil.com
return redirect(url)
# VULNERABLE: Login redirect
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
if authenticate(username, password):
# Attacker: /login?next=https://phishing-site.com
next_url = request.args.get('next', '/')
return redirect(next_url)
return 'Invalid credentials', 401
# VULNERABLE: Logout with redirect
@app.route('/logout')
def logout():
session.clear()
# Redirect to attacker site after logout
return redirect(request.args.get('redirect', '/'))
// VULNERABLE: Servlet redirect
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String url = request.getParameter("url");
// No validation - redirects anywhere!
response.sendRedirect(url);
}
}
// VULNERABLE: Spring Controller
@Controller
public class AuthController {
@GetMapping("/login")
public String login(@RequestParam(required = false) String returnUrl) {
// Store return URL for post-login redirect
// Attacker: /login?returnUrl=https://evil.com
return "login";
}
@PostMapping("/login")
public String processLogin(@RequestParam String returnUrl) {
// After successful login
return "redirect:" + returnUrl; // Open redirect!
}
}
// VULNERABLE: Express redirect
app.get('/goto', (req, res) => {
const url = req.query.url;
// Attacker: /goto?url=https://malicious.com
res.redirect(url);
});
// VULNERABLE: OAuth callback
app.get('/oauth/callback', (req, res) => {
const code = req.query.code;
const state = req.query.state; // Contains redirect URL
// Exchange code for token...
// Redirect to state URL - attacker controlled!
res.redirect(state);
});
// VULNERABLE: Partial validation bypass
app.get('/redirect', (req, res) => {
let url = req.query.url;
// Weak check - can be bypassed
if (url.includes('trusted.com')) {
res.redirect(url); // trusted.com.evil.com bypasses!
}
});
Fixed Code
# SAFE: Whitelist allowed redirect destinations
from flask import Flask, request, redirect, abort
from urllib.parse import urlparse
ALLOWED_HOSTS = {'example.com', 'www.example.com', 'app.example.com'}
def is_safe_url(url):
"""Check if URL is safe for redirect."""
if not url:
return False
# Allow relative URLs
if url.startswith('/') and not url.startswith('//'):
return True
# Parse and validate absolute URLs
try:
parsed = urlparse(url)
# Must be http or https
if parsed.scheme not in ('http', 'https'):
return False
# Must be in allowed hosts
return parsed.netloc in ALLOWED_HOSTS
except:
return False
@app.route('/redirect')
def redirect_handler():
url = request.args.get('url', '/')
if not is_safe_url(url):
abort(400, 'Invalid redirect URL')
return redirect(url)
# SAFE: Use indirect references
REDIRECT_MAPPING = {
'home': '/',
'dashboard': '/dashboard',
'profile': '/user/profile',
'settings': '/user/settings',
}
@app.route('/goto/<destination>')
def safe_redirect(destination):
url = REDIRECT_MAPPING.get(destination)
if not url:
abort(404, 'Unknown destination')
return redirect(url)
# SAFE: Login with validated redirect
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
if authenticate(username, password):
next_url = request.args.get('next', '/')
# Validate redirect URL
if not is_safe_url(next_url):
next_url = '/' # Default to home
return redirect(next_url)
return 'Invalid credentials', 401
// SAFE: Whitelist validation
@WebServlet("/redirect")
public class SafeRedirectServlet extends HttpServlet {
private static final Set<String> ALLOWED_DOMAINS = Set.of(
"example.com",
"www.example.com",
"app.example.com"
);
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String url = request.getParameter("url");
if (!isValidRedirectUrl(url)) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid redirect URL");
return;
}
response.sendRedirect(url);
}
private boolean isValidRedirectUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
// Allow relative URLs starting with /
if (url.startsWith("/") && !url.startsWith("//")) {
return true;
}
try {
URL parsedUrl = new URL(url);
String protocol = parsedUrl.getProtocol();
String host = parsedUrl.getHost();
// Only allow http/https
if (!protocol.equals("http") && !protocol.equals("https")) {
return false;
}
// Check against whitelist
return ALLOWED_DOMAINS.contains(host.toLowerCase());
} catch (MalformedURLException e) {
return false;
}
}
}
// SAFE: Spring Security approach
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.successHandler(new SafeAuthenticationSuccessHandler())
.and()
.logout()
.logoutSuccessHandler(new SafeLogoutSuccessHandler());
}
}
public class SafeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException {
String targetUrl = request.getParameter("redirect");
// Validate or use default
if (!isValidUrl(targetUrl)) {
targetUrl = "/dashboard";
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
}
// SAFE: Whitelist validation
const ALLOWED_HOSTS = new Set([
'example.com',
'www.example.com',
'app.example.com'
]);
function isValidRedirectUrl(url) {
if (!url) return false;
// Allow relative URLs
if (url.startsWith('/') && !url.startsWith('//')) {
return true;
}
try {
const parsed = new URL(url);
// Only allow http/https
if (!['http:', 'https:'].includes(parsed.protocol)) {
return false;
}
// Check whitelist
return ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());
} catch {
return false;
}
}
app.get('/goto', (req, res) => {
const url = req.query.url || '/';
if (!isValidRedirectUrl(url)) {
return res.status(400).send('Invalid redirect URL');
}
res.redirect(url);
});
// SAFE: OAuth with state validation
const crypto = require('crypto');
app.get('/oauth/start', (req, res) => {
// Generate secure state with embedded return URL
const returnUrl = req.query.return || '/';
const state = crypto.randomBytes(16).toString('hex');
// Store mapping server-side
req.session.oauthState = {
token: state,
returnUrl: isValidRedirectUrl(returnUrl) ? returnUrl : '/'
};
// Redirect to OAuth provider with state token only
res.redirect(`https://oauth.provider.com/auth?state=${state}&...`);
});
app.get('/oauth/callback', (req, res) => {
const { code, state } = req.query;
// Validate state matches session
if (!req.session.oauthState || req.session.oauthState.token !== state) {
return res.status(400).send('Invalid state');
}
// Get validated return URL from session, not request
const returnUrl = req.session.oauthState.returnUrl;
delete req.session.oauthState;
// Exchange code for token...
res.redirect(returnUrl);
});
Exploited in the Wild
Google Open Redirect (Multiple)
Google has had multiple open redirect vulnerabilities over the years in various services. Attackers used these in phishing campaigns, leveraging Google's trusted domain.
Facebook OAuth Open Redirect (2019)
An open redirect in Facebook's OAuth flow allowed attackers to steal access tokens by redirecting the OAuth callback to attacker-controlled sites.
PayPal Phishing via Open Redirect
PayPal has faced numerous phishing campaigns exploiting open redirects, where emails with legitimate PayPal URLs ultimately directed users to credential-harvesting sites.
Tools to test/exploit
-
Burp Suite — intercept and modify redirect parameters.
-
OWASP ZAP — automated open redirect detection.
-
OpenRedireX — open redirect fuzzer.
-
Nuclei Templates — open redirect detection templates.
CVE Examples
-
CVE-2021-22947 — cURL open redirect.
-
CVE-2020-7599 — Gradle open redirect.
-
CVE-2019-19844 — Django open redirect in password reset.
References
-
MITRE. "CWE-601: URL Redirection to Untrusted Site." https://cwe.mitre.org/data/definitions/601.html
-
OWASP. "Unvalidated Redirects and Forwards Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html