FlawOpen
● Redirecciones y CSRF / CWE-601, CWE-352

OAuth 2.0: ataques a redirect_uri, robo de códigos y login CSRF

Una validación permisiva de redirecciones en el servidor de autorización o un redireccionamiento abierto en un callback registrado puede exponer un código de autorización. La explotación aún depende de los controles de vinculación del código y del endpoint de tokens; la falta de vínculo del flujo de inicio de sesión también puede causar login CSRF.

💡 Explicación sencilla (ELI5)

Imagina el servidor de autorización como una oficina de correos de confianza: envía un código de un solo uso únicamente a la dirección de retorno registrada para una aplicación. Si esa dirección se compara de forma flexible, o el endpoint registrado reenvía a los visitantes a cualquier sitio, el código puede desviarse. Capturar el código no concede acceso por sí solo: el atacante también debe cumplir los requisitos de intercambio de tokens del cliente, incluidos PKCE y la autenticación del cliente cuando corresponda. Un fallo distinto de login CSRF ocurre cuando el sitio no puede comprobar si el navegador inició el inicio de sesión que está completando.

Conceptos principales y términos clave

Código de autorización
Credencial de corta duración que se devuelve mediante la redirección del navegador. El cliente la canjea en el endpoint de tokens; el canje queda vinculado al cliente y al URI de redirección y, si se usa PKCE, al verificador.
Coincidencia exacta de redirect_uri
RFC 9700 exige que el servidor de autorización compare el URI solicitado con uno prerregistrado mediante coincidencia exacta de cadenas, con una excepción limitada para el puerto dinámico de localhost en aplicaciones nativas.
state y login CSRF
Un valor impredecible vinculado a la sesión del navegador y comprobado en el callback puede asociar la respuesta con un inicio de sesión iniciado por el usuario. PKCE aplicado también puede proteger contra CSRF; usar ambos añade defensa en profundidad.
PKCE (Proof Key for Code Exchange)
El cliente crea un verificador secreto y envía su desafío derivado en la solicitud de autorización. El endpoint de tokens exige el verificador para canjear el código y reducir la interceptación en clientes públicos y confidenciales.

Rutas de ataque por redirección y login CSRF

Ruta A · 1

El servidor de autorización acepta un URI inseguro

Un servidor de autorización defectuoso usa un comodín, prefijo o subcadena en vez de comparar el redirect_uri solicitado con un valor registrado exacto. RFC 9700 exige coincidencia exacta, salvo la excepción del puerto localhost para aplicaciones nativas.

Ruta A · 2

Un endpoint registrado redirige a un sitio no confiable

El URI aceptado puede llegar a un redireccionamiento abierto controlado por el cliente, como https://app.example/redirect?next=.... Esto combina una validación débil del servidor de autorización con una debilidad CWE-601 independiente en la aplicación cliente.

Ruta A · 3

El navegador transporta el código por la redirección

Tras la autorización del usuario, el servidor devuelve el código al URI aceptado. Si el redireccionamiento abierto reenvía la solicitud conservando el código, el atacante puede capturarlo. El código no es un token de acceso bearer.

Ruta A · 4

El canje por tokens todavía debe funcionar

El atacante solo puede usar el código capturado si satisface las comprobaciones del endpoint de tokens. PKCE impide el canje sin el verificador original; la autenticación de clientes confidenciales y la coincidencia del URI añaden otros vínculos.

Ruta B

La falta de vínculo de respuesta permite login CSRF

Si el callback acepta una respuesta de autorización que no está vinculada al intento de inicio de sesión del navegador, el atacante puede hacer que la víctima complete un flujo asociado a la cuenta del atacante. Esto causa confusión de cuenta, no acceso automático a la cuenta de la víctima.

Código fuente: fallo e implementación segura

PATRÓN VULNERABLE
# 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
DEFENSA REFORZADA
# 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")

Lista de verificación de protección de ingeniería

← Explorar el directorio completo de seguridad Ver guías de vulnerabilidades → RFC 9700: mejores prácticas de seguridad de OAuth 2.0 → RFC 7636: Proof Key for Code Exchange (PKCE) → RFC 6749: manipulación del URI de redirección del código de autorización → CWE-601: redireccionamiento abierto → CWE-352: falsificación de solicitudes entre sitios →