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

AuswirkungDetails
ReputationBereich: Phishing-Enablement

Vertrauenswürdige Domains werden in Phishing-Angriffen verwendet und schädigen den Ruf der Organisation.
VertraulichkeitBereich: Credential-Diebstahl

Benutzer, die zu gefälschten Login-Seiten umgeleitet werden, können Credentials preisgeben.
ZugriffskontrolleBereich: 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

  1. MITRE. "CWE-601: URL Redirection to Untrusted Site." https://cwe.mitre.org/data/definitions/601.html
  2. OWASP. "Unvalidated Redirects and Forwards Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html