URL-Umleitung zu nicht vertrauenswürdiger Site (Open Redirect)
Beschreibung
URL-Umleitung zu nicht vertrauenswürdiger Site tritt auf, wenn eine Webanwendung eine benutzergesteuerte Eingabe akzeptiert, die einen Link zu einer externen Site angibt, und diesen Link zur Umleitung des Benutzers verwendet. Dies ermöglicht Angreifern, URLs zu erstellen, die scheinbar auf die legitime Site zeigen, aber Opfer zu bösartigen Sites umleiten. Open Redirects werden häufig in Phishing-Angriffen ausgenutzt, da Opfer die vertrauenswürdige Domain in der URL sehen, bevor sie klicken. Die Schwachstelle tritt auf Login-Seiten, Logout-Handlern und jeder Funktionalität auf, die basierend auf Benutzereingaben umleitet.
Risiko
Open Redirects sind ein bedeutender Phishing-Enabler. Angreifer erstellen URLs wie https://trusted-bank.com/redirect?url=https://evil-site.com, denen Opfer vertrauen, weil sie die legitime Domain sehen. Nach dem Klicken landen Opfer auf Angreifer-kontrollierten Sites, die Login-Seiten imitieren. Open Redirects können auch mit anderen Schwachstellen verkettet werden - sie können SSRF-Schutzmaßnahmen umgehen, OAuth-Tokens stehlen oder XSS durch javascript:-URLs ermöglichen. Obwohl oft als "niedrige Schwere" bewertet, macht ihre Rolle in Phishing-Kampagnen sie wirkungsvoll.
Lösung
Vermeiden Sie benutzergesteuerte Umleitungen, wenn möglich. Wenn Umleitungen notwendig sind, verwenden Sie eine Whitelist erlaubter Ziele. Validieren Sie Umleitungs-URLs gegen eine Liste vertrauenswürdiger Domains. Verwenden Sie indirekte Referenzen (Mapping-IDs zu URLs serverseitig) anstelle direkter URLs. Blockieren Sie javascript:, data: und andere gefährliche URL-Schemata. Für relative Umleitungen stellen Sie sicher, dass der Pfad keine Protokoll-Handler enthält. Zeigen Sie Zwischenwarnungen an, bevor Sie zu externen Sites umleiten.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Reputation | Bereich: Phishing-Enablement Vertrauenswürdige Domains werden in Phishing-Angriffen verwendet und schädigen den Ruf der Organisation. |
| Vertraulichkeit | Bereich: Credential-Diebstahl Benutzer, die zu gefälschten Login-Seiten umgeleitet werden, können Credentials preisgeben. |
| Zugriffskontrolle | Bereich: Token-Diebstahl OAuth- und SSO-Flows können Tokens durch Open Redirects leaken. |
Beispielcode + Lösungscode
Verwundbarer Code
# VERWUNDBAR: Direkte Umleitung aus Parameter
from flask import Flask, request, redirect
@app.route('/redirect')
def redirect_handler():
url = request.args.get('url')
# Angreifer: /redirect?url=https://evil.com
return redirect(url)
# VERWUNDBAR: Login-Umleitung
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
if authenticate(username, password):
# Angreifer: /login?next=https://phishing-site.com
next_url = request.args.get('next', '/')
return redirect(next_url)
return 'Ungültige Credentials', 401
# VERWUNDBAR: Logout mit Umleitung
@app.route('/logout')
def logout():
session.clear()
# Umleitung zu Angreifer-Site nach Logout
return redirect(request.args.get('redirect', '/'))
// VERWUNDBAR: Servlet-Umleitung
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String url = request.getParameter("url");
// Keine Validierung - leitet überall hin um!
response.sendRedirect(url);
}
}
// VERWUNDBAR: Spring Controller
@Controller
public class AuthController {
@GetMapping("/login")
public String login(@RequestParam(required = false) String returnUrl) {
// Return-URL für Post-Login-Umleitung speichern
// Angreifer: /login?returnUrl=https://evil.com
return "login";
}
@PostMapping("/login")
public String processLogin(@RequestParam String returnUrl) {
// Nach erfolgreicher Anmeldung
return "redirect:" + returnUrl; // Open Redirect!
}
}
// VERWUNDBAR: Express-Umleitung
app.get('/goto', (req, res) => {
const url = req.query.url;
// Angreifer: /goto?url=https://malicious.com
res.redirect(url);
});
// VERWUNDBAR: OAuth-Callback
app.get('/oauth/callback', (req, res) => {
const code = req.query.code;
const state = req.query.state; // Enthält Umleitungs-URL
// Code gegen Token tauschen...
// Umleitung zu state-URL - Angreifer-kontrolliert!
res.redirect(state);
});
// VERWUNDBAR: Partielle Validierungs-Bypass
app.get('/redirect', (req, res) => {
let url = req.query.url;
// Schwache Prüfung - kann umgangen werden
if (url.includes('trusted.com')) {
res.redirect(url); // trusted.com.evil.com umgeht dies!
}
});
Lösungscode
# SICHER: Whitelist erlaubter Umleitungsziele
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):
"""Prüft ob URL sicher für Umleitung ist."""
if not url:
return False
# Relative URLs erlauben
if url.startswith('/') and not url.startswith('//'):
return True
# Absolute URLs parsen und validieren
try:
parsed = urlparse(url)
# Muss http oder https sein
if parsed.scheme not in ('http', 'https'):
return False
# Muss in erlaubten Hosts sein
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, 'Ungültige Umleitungs-URL')
return redirect(url)
# SICHER: Indirekte Referenzen verwenden
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, 'Unbekanntes Ziel')
return redirect(url)
# SICHER: Login mit validierter Umleitung
@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', '/')
# Umleitungs-URL validieren
if not is_safe_url(next_url):
next_url = '/' # Standard auf Home
return redirect(next_url)
return 'Ungültige Credentials', 401
// SICHER: Whitelist-Validierung
@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, "Ungültige Umleitungs-URL");
return;
}
response.sendRedirect(url);
}
private boolean isValidRedirectUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
// Relative URLs, die mit / beginnen, erlauben
if (url.startsWith("/") && !url.startsWith("//")) {
return true;
}
try {
URL parsedUrl = new URL(url);
String protocol = parsedUrl.getProtocol();
String host = parsedUrl.getHost();
// Nur http/https erlauben
if (!protocol.equals("http") && !protocol.equals("https")) {
return false;
}
// Gegen Whitelist prüfen
return ALLOWED_DOMAINS.contains(host.toLowerCase());
} catch (MalformedURLException e) {
return false;
}
}
}
// SICHER: Spring Security-Ansatz
@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");
// Validieren oder Standard verwenden
if (!isValidUrl(targetUrl)) {
targetUrl = "/dashboard";
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
}
// SICHER: Whitelist-Validierung
const ALLOWED_HOSTS = new Set([
'example.com',
'www.example.com',
'app.example.com'
]);
function isValidRedirectUrl(url) {
if (!url) return false;
// Relative URLs erlauben
if (url.startsWith('/') && !url.startsWith('//')) {
return true;
}
try {
const parsed = new URL(url);
// Nur http/https erlauben
if (!['http:', 'https:'].includes(parsed.protocol)) {
return false;
}
// Whitelist prüfen
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('Ungültige Umleitungs-URL');
}
res.redirect(url);
});
// SICHER: OAuth mit State-Validierung
const crypto = require('crypto');
app.get('/oauth/start', (req, res) => {
// Sicheren State mit eingebetteter Return-URL generieren
const returnUrl = req.query.return || '/';
const state = crypto.randomBytes(16).toString('hex');
// Mapping serverseitig speichern
req.session.oauthState = {
token: state,
returnUrl: isValidRedirectUrl(returnUrl) ? returnUrl : '/'
};
// Zu OAuth-Provider umleiten mit nur State-Token
res.redirect(`https://oauth.provider.com/auth?state=${state}&...`);
});
app.get('/oauth/callback', (req, res) => {
const { code, state } = req.query;
// State gegen Session validieren
if (!req.session.oauthState || req.session.oauthState.token !== state) {
return res.status(400).send('Ungültiger State');
}
// Validierte Return-URL aus Session holen, nicht aus Request
const returnUrl = req.session.oauthState.returnUrl;
delete req.session.oauthState;
// Code gegen Token tauschen...
res.redirect(returnUrl);
});
Ausgenutzt in der Praxis
Google Open Redirect (Mehrfach)
Google hatte im Laufe der Jahre mehrere Open-Redirect-Schwachstellen in verschiedenen Diensten. Angreifer nutzten diese in Phishing-Kampagnen und nutzten Googles vertrauenswürdige Domain.
Facebook OAuth Open Redirect (2019)
Ein Open Redirect in Facebooks OAuth-Flow ermöglichte Angreifern, Zugangstokens zu stehlen, indem der OAuth-Callback zu Angreifer-kontrollierten Sites umgeleitet wurde.
PayPal Phishing via Open Redirect
PayPal war mit zahlreichen Phishing-Kampagnen konfrontiert, die Open Redirects ausnutzten, wobei E-Mails mit legitimen PayPal-URLs Benutzer letztendlich zu Credential-Harvesting-Sites leiteten.
Tools zum Testen/Ausnutzen
- Burp Suite — Umleitungsparameter abfangen und modifizieren
- OWASP ZAP — automatisierte Open-Redirect-Erkennung
- OpenRedireX — Open-Redirect-Fuzzer
- Nuclei Templates — Open-Redirect-Erkennungsvorlagen
CVE-Beispiele
- CVE-2021-22947 — cURL Open Redirect
- CVE-2020-7599 — Gradle Open Redirect
- CVE-2019-19844 — Django Open Redirect in Passwort-Reset
Referenzen
- 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