Skip to content

Services, Wiki-Artikel, Blog-Beiträge und Glossar-Einträge durchsuchen

↑↓NavigierenEnterÖffnenESCSchließen
Web-Sicherheit Glossary

Open Redirect - Offene Weiterleitung

An open redirect is a web application vulnerability in which an attacker exploits the redirect functionality of a legitimate website to redirect users to an external attacker-controlled website. Although it does not technically involve direct code execution, Open Redirect is used as a catalyst for phishing (legitimate URL misleads about the destination), OAuth/OIDC token theft (redirect_uri manipulation), and as an SSRF tool. In OWASP API Security, Open Redirect is relevant to A3:2023 (Broken Object Property Level Authorization).

Open Redirect sounds harmless—and is often dismissed by many developers as a low-severity finding. But when combined with OAuth 2.0, a "harmless" redirect turns into a critical token theft scenario. And as a phishing vector, https://legitime-bank.de/redirect?url=https://evil.com/login is nearly impossible for victims to detect. Open Redirect regularly appears in bug bounty reports—often with substantial bounties when combined with OAuth.

Open Redirect: Basic Principle

Typical Implementation with Vulnerability:

Legitimate Use:
  # After Login: Redirect user back to the requested page
  GET /login?redirect=/dashboard
  → After Login: 302 Location: /dashboard ✓

Vulnerability:
  GET /login?redirect=https://evil.com/fake-login
  → After login: 302 Location: https://evil.com/fake-login ✗

  Or:
  GET /logout?next=https://phishing.com/
  GET /redirect?url=https://evil.com/

Common parameter names for Open Redirect:
  url=, redirect=, next=, return=, returnTo=, redirect_uri=
  dest=, destination=, target=, rurl=, forward=, goto=
  continue=, back=, from=, origin=, to=, out=

Attack Scenarios

Phishing Attack via Open Redirect:

  # Normal phishing link:
  https://evil.com/fake-banking-login/
  → Security-conscious user: suspicious domain detected!

  # With open redirect on a legitimate bank domain:
  https://real-bank.de/logout?redirect=https://evil.com/fake-banking-login/
  → URL starts with a real bank domain!
  → Email filter: real domain in the link – less suspicious
  → User sees: "https://real-bank.de/..." → clicks!
  → Lands on phishing page

---

OAuth/OIDC Token Theft:

  # Normal OAuth flow:
  GET /authorize?client_id=myapp&redirect_uri=https://myapp.com/callback&...
  → After auth: Code sent to https://myapp.com/callback (valid!)

  # With Open Redirect to Authorization Server:
  # If Authorization Server itself has Open Redirect:
  https://auth.example.com/authorize?client_id=myapp&
    redirect_uri=https://auth.example.com/redirect?url=https://evil.com/

  # Flow:
  1. Auth Server redirects after login to:
     https://auth.example.com/redirect?url=https://evil.com/&code;=SECRET_CODE
  2. Open Redirect forwards to:
     https://evil.com/?code=SECRET_CODE
  3. Attacker steals the authorization code!
  4. Exchanges code for access token

  → Critical combination! Code Severity: High/Critical

---

SSRF via Open Redirect:

  # SSRF filter only checks external URLs:
  # If the app has SSRF protection but does not account for open redirects:

  fetch?url=https://legitimate-site.com/redirect?url=http://169.254.169.254/

  # Protective measure (apparently):
  → URL is checked: legitimate-site.com → allowed ✓

  # Exploit:
  → Server follows redirect to http://169.254.169.254/
  → SSRF protection bypassed!

---

Credential Theft via Referrer Header:

  # If external site: Authorization code in URL + Referrer:
  # User comes from:
  https://auth.site.com/callback?code=SECRET&state;=xyz
  → Clicks on link on callback page to external provider
  → Referrer header contains: https://auth.site.com/callback?code=SECRET!
  → Code leaked to external site!

Detection and Prevention

Programmatic protection:

Allowlist-based redirect (Best Practice):
  # Python (Flask):
  from urllib.parse import urlparse

  ALLOWED_DOMAINS = {'myapp.com', 'www.myapp.com', 'api.myapp.com'}

  def safe_redirect(url: str, default: str = '/') -> str:
      """Redirect only to allowed domains"""
      if not url:
          return default

      parsed = urlparse(url)

      # Allow relative URLs (no scheme → internal):
      if not parsed.netloc and not parsed.scheme:
          # Only /path/... - no domain specified
          return url

      # External URLs: Domain check
      if parsed.netloc.rstrip('/') not in ALLOWED_DOMAINS:
          return default  # Unknown domain → Default

      # Scheme check (no javascript:, data:, etc.)
      if parsed.scheme not in ('http', 'https'):
          return default

      return url

  # Usage:
  redirect_url = request.args.get('redirect', '/')
  return redirect(safe_redirect(redirect_url))

JavaScript (Next.js):
  // Middleware or API route:
  function safeRedirect(url: string, fallback = '/'): string {
      try {
          // Allow relative URLs directly
          if (url.startsWith('/') && !url.startsWith('//')) {
              return url;
          }
          const parsed = new URL(url);
          const allowed = ['myapp.com'];
          if (allowed.includes(parsed.hostname)) {
              return url;
          }
          return fallback;
      } catch {
          return fallback;  // Invalid URL
      }
  }

PHP:
  function safe_redirect(string $url, string $default = '/'): string {
      // Only relative paths and own domain
      $parsed = parse_url($url);
      if (!isset($parsed['host'])) {
          return $url; // Relative URL
      }
      $allowed = ['example.com', 'www.example.com'];
      return in_array($parsed['host'], $allowed) ? $url : $default;
  }

Common errors:
  # Insufficient validation (can be bypassed!):

  # Only check prefix - bypassable with ?:
  if starts_with(url, 'https://mysite.com'):
      redirect(url)
  # Bypass: https://mysite.com.evil.com/ or https://mysite.com@evil.com/

  # Host match only - bypassable with @:
  parsed = urlparse(url)
  if parsed.netloc == 'mysite.com':
      redirect(url)
  # Bypass: https://mysite.com@evil.com/ → netloc is 'mysite.com@evil.com'
  # → parsed.netloc != 'mysite.com' BUT browser navigates to evil.com!

  # Forgot JavaScript URIs:
  if not url.startswith('http://evil'):
      redirect(url)
  # Bypass: javascript:alert(1) or data:text/html,...

---

OAuth-specific measures:

  # Authorization Server: Match redirect_uri EXACTLY!
  # No wildcards, no prefix match – only exact match!

  # Bad:
  allowed_uris = ['https://myapp.com/callback*']  # Wildcard – dangerous!
  if url.startswith(allowed_uris):  # Prefix – dangerous!

  # Good:
  ALLOWED_REDIRECT_URIS = {
      'https://myapp.com/callback',
      'https://myapp.com/oauth/callback',
  }
  if redirect_uri not in ALLOWED_REDIRECT_URIS:
      return error('invalid_redirect_uri')

  # State parameter for CSRF protection (against session fixation):
  state = secrets.token_urlsafe(32)
  session['oauth_state'] = state
  # → On callback: Check state from session!

Testing in the Penetration Test

Open Redirect Testing:

Parameter Discovery:
  # Burp Intruder: test all parameters with redirect semantics
  # Wordlist: url, redirect, next, return, goto, dest, ...

  # Manual: Search for link elements in the response
  grep -E "(href|action|src|redirect|url)=[\"']/" response.html

Test Payloads:
  # External Domain:
  ?redirect=https://evil.com/
  ?redirect=//evil.com/          ← Protocol-relative URL
  ?redirect=https:evil.com       ← Missing slashes (some browsers!)

  # Encoding Variants:
  ?redirect=%68%74%74%70%73%3a%2f%2fevil.com  ← URL-encoded
  ?redirect=https://evil%2Ecom/              ← Dot-encoded

  # Bypass attempts:
  ?redirect=https://legitimate-site.evil.com/
  ?redirect=https://legitimate-site.com.evil.com/
  ?redirect=https://legitimate-site.com@evil.com/

  # JavaScript URI:
  ?redirect=javascript:alert(1)
  ?redirect=data:text/html,<script>alert(1)</script># SSRF via Redirect:
  ?redirect=http://169.254.169.254/latest/meta-data/

  # For OAuth combination:
  # Test redirect_uri with Open Redirect chain