OAuth 2.0 : attaques redirect_uri, vol de code et login CSRF
Une validation permissive des redirections par le serveur d’autorisation ou une redirection ouverte sur un callback enregistré peut exposer un code d’autorisation. L’exploitation dépend toujours des contrôles de liaison du code et du point de terminaison des jetons ; l’absence de liaison du flux de connexion peut aussi provoquer un login CSRF.
💡 Explication simple (ELI5)
Imaginez le serveur d’autorisation comme un bureau de poste de confiance : il n’envoie un code à usage unique qu’à l’adresse de retour enregistrée pour l’application. Si cette adresse est comparée de façon trop souple, ou si le point de terminaison enregistré renvoie les visiteurs vers n’importe quel site, le code peut être détourné. Capturer le code ne donne pas automatiquement accès : l’attaquant doit également satisfaire aux exigences d’échange de jetons du client, notamment PKCE et l’authentification du client lorsqu’elle est requise. Une faille distincte de login CSRF survient si le site ne peut pas vérifier que le navigateur a commencé la connexion qu’il termine.
Concepts clés et termes importants
- Code d’autorisation
- Identifiant de courte durée renvoyé par la redirection du navigateur. Le client l’échange auprès du point de terminaison des jetons ; cet échange est lié au client et à l’URI de redirection, ainsi qu’au vérificateur PKCE le cas échéant.
- Correspondance exacte de
redirect_uri - La RFC 9700 exige que le serveur d’autorisation compare l’URI demandé à un URI pré-enregistré par correspondance exacte de chaîne, sauf exception limitée pour le port dynamique localhost des applications natives.
stateet login CSRF- Une valeur imprévisible liée à la session du navigateur et vérifiée au callback peut associer la réponse à une connexion initiée par l’utilisateur. PKCE imposé peut aussi protéger contre le CSRF ; utiliser les deux ajoute une défense en profondeur.
- PKCE (Proof Key for Code Exchange)
- Le client crée un vérificateur secret et envoie son challenge dérivé dans la requête d’autorisation. Le point de terminaison des jetons exige ce vérificateur pour échanger le code, réduisant le risque d’interception pour les clients publics et confidentiels.
Chemins d’attaque par redirection et login CSRF
Le serveur d’autorisation accepte une URI non sûre
Un serveur d’autorisation défectueux utilise un joker, un préfixe ou une sous-chaîne au lieu de comparer redirect_uri à une valeur enregistrée exacte. La RFC 9700 impose la correspondance exacte, sauf l’exception spécifiée pour le port localhost des applications natives.
Un point de terminaison enregistré redirige vers un site non fiable
L’URI accepté peut atteindre une redirection ouverte contrôlée par le client, par exemple https://app.example/redirect?next=.... Cela combine une vérification faible du serveur d’autorisation et une faille CWE-601 distincte dans l’application cliente.
Le navigateur transporte le code dans la redirection
Après l’autorisation de l’utilisateur, le serveur renvoie le code à l’URI accepté. Si la redirection ouverte transmet la requête en conservant le code, l’attaquant peut le capturer. Le code seul n’est pas un jeton d’accès bearer.
L’échange contre un jeton doit encore réussir
L’attaquant ne peut utiliser le code capturé que s’il satisfait aux contrôles du point de terminaison des jetons. PKCE bloque l’échange sans le vérificateur d’origine ; l’authentification d’un client confidentiel et la correspondance de l’URI ajoutent d’autres liaisons.
Une réponse non liée permet le login CSRF
Si le callback accepte une réponse d’autorisation qui n’est pas liée à la tentative de connexion du navigateur, un attaquant peut amener la victime à terminer un flux associé au compte de l’attaquant. Il s’agit d’une confusion de compte, pas d’un accès automatique au compte de la victime.
Code source : faille et implémentation sécurisée
# 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")
Liste de contrôle du durcissement technique
- Configurez le serveur d’autorisation pour comparer
redirect_urià un URI pré-enregistré par correspondance exacte de chaîne. Ne conservez que l’exception RFC 9700 pour les ports localhost des applications natives. - N’intégrez pas de destination contrôlée par l’utilisateur dans l’URI de callback OAuth. Supprimez les redirections ouvertes des callbacks et n’acceptez que des identifiants de routes locales sur liste d’autorisation.
- Utilisez le flux de code d’autorisation avec PKCE S256 pour tous les types de clients. Envoyez le même
redirect_urienregistré dans les requêtes d’autorisation et de jeton. - Liez le callback à la tentative de connexion : vérifiez un
stateà usage unique lié à la session, ou ne comptez sur PKCE contre le CSRF que si le support du serveur d’autorisation est imposé. Utiliser les deux constitue une défense en profondeur. - Un code capturé ne suffit pas. Exigez aussi l’authentification correcte du client et le vérificateur PKCE pour échanger le jeton ; ne placez jamais de jeton d’accès dans une URL de redirection.
- Évitez de journaliser les codes d’autorisation et définissez une
Referrer-Policyrestrictive sur les pages de callback ; ne chargez aucun contenu tiers avant d’avoir supprimé les paramètres sensibles.