FlawOpen
● Redirection et CSRF / CWE-601, CWE-352

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.
state et 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

Parcours A · 1

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.

Parcours A · 2

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.

Parcours A · 3

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.

Parcours A · 4

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.

Parcours B

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

MODÈLE VULNÉRABLE
# 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
DÉFENSE RENFORCÉE
# 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

← Parcourir le répertoire complet de sécurité Explorer les guides de vulnérabilités → RFC 9700 : bonnes pratiques de sécurité OAuth 2.0 → RFC 7636 : Proof Key for Code Exchange (PKCE) → RFC 6749 : manipulation de l’URI de redirection du code d’autorisation → CWE-601 : redirection ouverte → CWE-352 : falsification de requête intersites →