OAuth 2.0: redirect_uri-Angriffe, Code-Diebstahl und Login-CSRF
Eine zu großzügige Redirect-Prüfung des Authorization Servers oder eine offene Weiterleitung auf einem registrierten Callback kann einen Authorization Code offenlegen. Eine Ausnutzung hängt weiterhin von Code-Bindung und Token-Endpunkt-Kontrollen ab; fehlende Bindung des Login-Flows kann außerdem Login-CSRF ermöglichen.
💡 Einfach erklärt
Stellen Sie sich den Authorization Server als vertrauenswürdige Poststelle vor: Er schickt einen einmaligen Code nur an die für eine App registrierte Rücksendeadresse. Wird diese Adresse zu locker abgeglichen oder leitet der registrierte Endpunkt Besucher an beliebige Websites weiter, kann der Code umgeleitet werden. Ein abgefangener Code gewährt nicht automatisch Zugriff: Der Angreifer muss auch die Anforderungen beim Token-Tausch erfüllen, einschließlich PKCE und Client-Authentifizierung, sofern erforderlich. Eine separate Login-CSRF-Schwachstelle liegt vor, wenn die Website nicht feststellen kann, ob der Browser den gerade abgeschlossenen Login selbst begonnen hat.
Kernkonzepte und wichtige Begriffe
- Authorization Code
- Kurzlebige Berechtigung, die per Browser-Redirect zurückgegeben wird. Der Client tauscht sie am Token-Endpunkt ein; die Einlösung ist an Client und Redirect-URI sowie bei PKCE an den Verifier gebunden.
- Exakter Abgleich von
redirect_uri - RFC 9700 verlangt, dass der Authorization Server die angeforderte Redirect-URI exakt als Zeichenfolge mit einer vorregistrierten URI vergleicht. Eine enge Ausnahme gilt für dynamische localhost-Ports nativer Apps.
stateund Login-CSRF- Ein nicht vorhersagbarer, an die Browsersitzung gebundener und beim Callback geprüfter Wert kann die Antwort einem vom Nutzer gestarteten Login zuordnen. Erzwingtes PKCE kann ebenfalls vor CSRF schützen; beides zusammen bietet Defense in Depth.
- PKCE (Proof Key for Code Exchange)
- Der Client erzeugt einen geheimen Code-Verifier und sendet dessen abgeleiteten Challenge in der Autorisierungsanfrage. Der Token-Endpunkt verlangt den Verifier zur Code-Einlösung und reduziert so das Abfangrisiko für öffentliche und vertrauliche Clients.
Schrittweiser Ablauf von Redirect- und Login-CSRF-Angriffen
Authorization Server akzeptiert eine unsichere Redirect-URI
Ein fehlerhafter Authorization Server verwendet Wildcard-, Präfix- oder Teilstring-Prüfungen statt redirect_uri exakt mit einem registrierten Wert zu vergleichen. RFC 9700 verlangt exakten Abgleich, außer bei der definierten localhost-Port-Ausnahme für native Apps.
Ein registrierter Endpunkt leitet zu einer nicht vertrauenswürdigen Site weiter
Die akzeptierte URI kann einen vom Client kontrollierten offenen Redirect erreichen, etwa https://app.example/redirect?next=.... Hier treffen eine lockere Prüfung des Authorization Servers und eine separate CWE-601-Schwachstelle in der Client-App zusammen.
Der Browser trägt den Code durch die Weiterleitung
Nach der Nutzerfreigabe sendet der Server den Code an die akzeptierte URI zurück. Leitet der offene Redirect die Anfrage samt Code weiter, kann der Angreifer ihn abfangen. Der Code allein ist kein Bearer-Access-Token.
Der Token-Tausch muss weiterhin gelingen
Der Angreifer kann einen abgefangenen Code nur verwenden, wenn er die Prüfungen des Token-Endpunkts erfüllt. PKCE verhindert die Einlösung ohne den ursprünglichen Verifier; Client-Authentifizierung vertraulicher Clients und URI-Abgleich binden zusätzlich.
Fehlende Antwortbindung ermöglicht Login-CSRF
Akzeptiert der Callback eine Autorisierungsantwort, die nicht an den Login-Versuch des Browsers gebunden ist, kann ein Angreifer das Opfer dazu bringen, einen Flow für das Konto des Angreifers abzuschließen. Das führt zu Kontoverwechslung, nicht automatisch zum Zugriff auf das Opferkonto.
Quellcode: Fehler und sichere Implementierung
# VULNERABLE: user input is embedded in redirect_uri; state and PKCE are absent.
from urllib.parse import urlencode
from flask import Flask, request, redirect
app = Flask(__name__)
AUTHORIZATION_ENDPOINT = "https://oauth-provider.example/authorize"
@app.route("/auth/login")
def oauth_login():
next_url = request.args.get("next", "")
# The callback is an open redirect, and this URI varies with user input.
redirect_uri = "https://app.example/redirect?next=" + next_url
params = {
"client_id": "example-client",
"response_type": "code",
"redirect_uri": redirect_uri,
}
# Exploitation also requires the authorization server to accept this URI
# under an unsafe wildcard or prefix rule.
return redirect(AUTHORIZATION_ENDPOINT + "?" + urlencode(params))
@app.route("/auth/callback")
def oauth_callback():
# No state check ties this response to the browser's login attempt.
code = request.args.get("code")
return exchange_code_for_tokens(code) # no PKCE verifier provided
# SECURE: fixed registered redirect URI, session-bound state, and PKCE.
import base64
import hashlib
import secrets
from urllib.parse import urlencode
from flask import Flask, request, redirect, session, abort
app = Flask(__name__)
AUTHORIZATION_ENDPOINT = "https://oauth-provider.example/authorize"
REDIRECT_URI = "https://app.example/auth/callback" # exact registered value
CLIENT_ID = "example-client"
@app.route("/auth/login")
def oauth_login():
state = secrets.token_urlsafe(32)
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode("ascii")).digest()
).rstrip(b"=").decode("ascii")
session["oauth_state"] = state
session["pkce_verifier"] = verifier
params = {
"client_id": CLIENT_ID,
"response_type": "code",
"redirect_uri": REDIRECT_URI,
"state": state,
"code_challenge": challenge,
"code_challenge_method": "S256",
}
return redirect(AUTHORIZATION_ENDPOINT + "?" + urlencode(params))
@app.route("/auth/callback")
def oauth_callback():
expected = session.pop("oauth_state", None)
if not expected or not secrets.compare_digest(
request.args.get("state", ""), expected
):
abort(403, "Invalid OAuth state")
code = request.args.get("code")
verifier = session.pop("pkce_verifier", None)
if not code or not verifier:
abort(400, "Missing authorization response")
# The token request must use this exact redirect_uri and verifier.
tokens = exchange_code_for_tokens(
code=code, redirect_uri=REDIRECT_URI, code_verifier=verifier
)
session["user_id"] = tokens["user_id"]
return redirect("/dashboard")
Checkliste zur technischen Absicherung
- Konfigurieren Sie den Authorization Server so, dass
redirect_uriexakt als Zeichenfolge mit einer vorregistrierten URI verglichen wird. Behalten Sie nur die RFC-9700-Ausnahme für localhost-Ports nativer Apps bei. - Übernehmen Sie kein nutzergesteuertes Ziel in eine OAuth-Callback-URI. Entfernen Sie offene Redirects aus Callback-Pfaden und akzeptieren Sie nur lokal allowgelistete Routenkennungen.
- Verwenden Sie den Authorization-Code-Flow mit PKCE S256 für alle Client-Typen. Senden Sie dieselbe registrierte
redirect_uriin Autorisierungs- und Token-Anfrage. - Binden Sie den Callback an den Login-Versuch: Prüfen Sie einen einmaligen, sitzungsgebundenen
stateoder verlassen Sie sich auf PKCE gegen CSRF nur, wenn PKCE-Unterstützung des Authorization Servers erzwungen wird. Beides zusammen ist Defense in Depth. - Ein abgefangener Code reicht nicht aus. Verlangen Sie für den Token-Tausch auch korrekte Client-Authentifizierung und den PKCE-Verifier; setzen Sie niemals Access-Tokens in Redirect-URLs.
- Protokollieren Sie keine Authorization Codes und setzen Sie auf Callback-Seiten eine restriktive
Referrer-Policy; laden Sie keine Drittinhalte, bevor sensible URL-Parameter entfernt sind.