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.
statey 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
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.
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.
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.
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.
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
# 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")
Lista de verificación de protección de ingeniería
- Configura el servidor de autorización para comparar
redirect_uricon un URI prerregistrado mediante coincidencia exacta de cadenas. Conserva únicamente la excepción de RFC 9700 para puertos localhost de aplicaciones nativas. - No incluyas un destino controlado por el usuario en el URI del callback OAuth. Elimina los redireccionamientos abiertos de las rutas de callback y acepta solo identificadores locales incluidos en una lista permitida.
- Usa el flujo de código de autorización con PKCE S256 para todos los tipos de cliente. Envía el mismo
redirect_uriregistrado en las solicitudes de autorización y de tokens. - Vincula el callback con el intento de inicio de sesión: valida un
statede un solo uso ligado a la sesión o confía en PKCE para CSRF solo si el soporte del servidor de autorización está garantizado. Usar ambos aporta defensa en profundidad. - Capturar un código no basta. Exige también la autenticación de cliente y el verificador PKCE correctos para canjearlo; nunca pongas tokens de acceso en URLs de redirección.
- Evita registrar códigos de autorización y configura una
Referrer-Policyrestrictiva en las páginas de callback; no cargues contenido de terceros antes de quitar los parámetros sensibles de la URL.