Open Redirect - Offene Weiterleitung
Open Redirect ist eine Webanwendungsschwachstelle bei der ein Angreifer die Redirect-Funktionalität einer legitimen Website missbraucht um Nutzer auf eine externe Angreifer-Website umzuleiten. Obwohl technisch keine direkte Code-Execution, wird Open Redirect als Phishing-Katalysator (legitime URL täuscht über Ziel), OAuth/OIDC Token-Diebstahl (redirect_uri Manipulation) und SSRF-Hilfsmittel eingesetzt. In OWASP API Security ist Open Redirect relevant für A3:2023 (Broken Object Property Level Authorization).
Open Redirect klingt harmlos - und wird von vielen Entwicklern als Low-Severity-Finding abgetan. Aber kombiniert mit OAuth 2.0 wird aus einem “harmlosen” Redirect ein kritisches Token-Diebstahl-Szenario. Und als Phishing-Vehikel ist https://legitime-bank.de/redirect?url=https://evil.com/login für Opfer fast unmöglich zu erkennen. Open Redirect taucht regelmäßig in Bug-Bounty-Reports auf - oft mit beachtlichen Prämien wenn OAuth-Kombination gefunden.
Open Redirect Grundprinzip
Typische Implementierung mit Schwachstelle:
Legitime Verwendung:
# Nach Login: Nutzer zurück zur angeforderten Seite leiten
GET /login?redirect=/dashboard
→ Nach Login: 302 Location: /dashboard ✓
Schwachstelle:
GET /login?redirect=https://evil.com/fake-login
→ Nach Login: 302 Location: https://evil.com/fake-login ✗
Oder:
GET /logout?next=https://phishing.com/
GET /redirect?url=https://evil.com/
Häufige Parameter-Namen für Open Redirect:
url=, redirect=, next=, return=, returnTo=, redirect_uri=
dest=, destination=, target=, rurl=, forward=, goto=
continue=, back=, from=, origin=, to=, out=
Angriffs-Szenarien
Phishing-Angriff via Open Redirect:
# Normaler Phishing-Link:
https://evil.com/fake-banking-login/
→ Sicherheitsbewusster User: verdächtige Domain erkannt!
# Mit Open Redirect auf legitimer Bank-Domain:
https://real-bank.de/logout?redirect=https://evil.com/fake-banking-login/
→ URL beginnt mit echter Bank-Domain!
→ E-Mail-Filter: echte Domain im Link - weniger verdächtig
→ User sieht: "https://real-bank.de/..." → klickt!
→ Landet auf Phishing-Seite
---
OAuth/OIDC Token-Diebstahl:
# Normaler OAuth-Flow:
GET /authorize?client_id=myapp&redirect_uri=https://myapp.com/callback&...
→ Nach Auth: Code gesendet an https://myapp.com/callback (valide!)
# Mit Open Redirect auf Authorization Server:
# Wenn Authorization Server selbst Open Redirect hat:
https://auth.example.com/authorize?client_id=myapp&
redirect_uri=https://auth.example.com/redirect?url=https://evil.com/
# Flow:
1. Auth Server redirectet nach Login zu:
https://auth.example.com/redirect?url=https://evil.com/&code=SECRET_CODE
2. Open Redirect leitet weiter zu:
https://evil.com/?code=SECRET_CODE
3. Angreifer stiehlt den Authorization Code!
4. Tauscht Code gegen Access Token
→ Kritische Kombination! Code-Severity: High/Critical
---
SSRF via Open Redirect:
# SSRF-Filter prüft nur externe URL:
# Wenn App SSRF-Schutz hat aber Open Redirect nicht berücksichtigt:
fetch?url=https://legitimate-site.com/redirect?url=http://169.254.169.254/
# Schutzmaßnahme (scheinbar):
→ URL wird geprüft: legitimate-site.com → erlaubt ✓
# Exploit:
→ Server folgt Redirect zu http://169.254.169.254/
→ SSRF-Schutz umgangen!
---
Credential-Theft via Referrer-Header:
# Wenn externe Seite: Authorization Code in URL + Referrer:
# User kommt von:
https://auth.site.com/callback?code=SECRET&state=xyz
→ Klickt auf Link auf Callback-Seite zu externem Provider
→ Referrer-Header enthält: https://auth.site.com/callback?code=SECRET!
→ Code an External Site geleaked!
Erkennung und Prävention
Programmatischer Schutz:
Allowlist-basierter 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:
"""Nur auf erlaubte Domains redirecten"""
if not url:
return default
parsed = urlparse(url)
# Relative URLs erlauben (kein Schema → intern):
if not parsed.netloc and not parsed.scheme:
# Nur /pfad/... - keine Domain-Angabe
return url
# Externe URLs: Domain-Prüfung
if parsed.netloc.rstrip('/') not in ALLOWED_DOMAINS:
return default # Unbekannte Domain → Default
# Schema-Prüfung (kein 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 oder API Route:
function safeRedirect(url: string, fallback = '/'): string {
try {
// Relative URLs direkt erlauben
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; // Ungültige URL
}
}
PHP:
function safe_redirect(string $url, string $default = '/'): string {
// Nur relative Pfade und eigene 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;
}
Häufige Fehler:
# Unzureichende Prüfung (bypassbar!):
# Nur Präfix prüfen - bypassbar mit ?:
if starts_with(url, 'https://mysite.com'):
redirect(url)
# Bypass: https://mysite.com.evil.com/ oder https://mysite.com@evil.com/
# Nur Host-Match - bypassbar mit @:
parsed = urlparse(url)
if parsed.netloc == 'mysite.com':
redirect(url)
# Bypass: https://mysite.com@evil.com/ → netloc ist 'mysite.com@evil.com'
# → parsed.netloc != 'mysite.com' ABER Browser navigiert zu evil.com!
# JavaScript-URI vergessen:
if not url.startswith('http://evil'):
redirect(url)
# Bypass: javascript:alert(1) oder data:text/html,...
---
OAuth-spezifische Maßnahmen:
# Authorization Server: redirect_uri EXAKT matchen!
# Keine Wildcards, kein Prefix-Match - nur exacte Übereinstimmung!
# Schlecht:
allowed_uris = ['https://myapp.com/callback*'] # Wildcard - gefährlich!
if url.startswith(allowed_uris): # Prefix - gefährlich!
# Gut:
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 für CSRF-Schutz (gegen Session-Fixation):
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
# → Bei Callback: state aus Session prüfen!
Testing im Pentest
Open Redirect Testing:
Parameter-Discovery:
# Burp Intruder: alle Parameter mit Redirect-Semantik testen
# Wordlist: url, redirect, next, return, goto, dest, ...
# Manuell: Link-Elemente im Response durchsuchen
grep -E "(href|action|src|redirect|url)=[\"']/" response.html
Test-Payloads:
# Externe Domain:
?redirect=https://evil.com/
?redirect=//evil.com/ ← Protocol-relative URL
?redirect=https:evil.com ← Fehlende Slashes (einige Browser!)
# Encoding-Varianten:
?redirect=%68%74%74%70%73%3a%2f%2fevil.com ← URL-encoded
?redirect=https://evil%2Ecom/ ← Dot-encoded
# Bypass-Versuche:
?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/
# Für OAuth-Kombination:
# redirect_uri mit Open-Redirect-Kette testen