Forced OAuth profile linking
CSRF en el flujo de vinculación de perfil OAuth: ausencia del parámetro state permite forzar al admin a vincular el perfil social del atacante mediante un iframe con el code de oauth-linking.
Target Overview & Vulnerability Summary
El laboratorio permite adjuntar un perfil de red social a la cuenta del blog para poder iniciar sesión vía OAuth en lugar de usuario/password. La implementación insegura del flujo OAuth por parte de la app cliente permite a un atacante vincular por la fuerza su propio perfil social a la cuenta de otro usuario (el admin) mediante una CSRF: el endpoint de vinculación no emite ni valida el parámetro state, por lo que un oauth-linking code emitido para la cuenta del atacante puede ser ejecutado por el admin.
| Campo | Valor |
|---|---|
| Severidad | Alta (account linking forzado → acceso a la cuenta admin del blog y eliminación de carlos) |
| Endpoint vulnerable | GET /oauth-linking?code=... (vinculación de perfil OAuth) |
| Root cause | Ausencia del parámetro state en el flujo de account linking → el code no está ligado a la sesión que inició el flujo (CSRF) |
Technical Breakdown & Exploitation Flow
Credenciales de laboratorio:
- Cuenta del blog:
wiener:peter - Perfil social:
peter.wiener:hotdog
Al observar el flujo, la app usa el authorization code grant type para el linking de perfiles, pero no envía el parámetro state. state es el mecanismo que previene ataques tipo CSRF: sin él, un flujo de OAuth que no fue iniciado por el cliente puede completarse igualmente. Concretamente, el code de vinculación emitido por el proveedor no está ligado a la sesión del navegador que lo solicitó.
Explotación
- Iniciar el flujo de vinculación de un perfil social en la cuenta
wiener(opción "Attach a social profile"). - Interceptar la request de callback de vinculación que contiene el
codedeoauth-linkingy droppearla (no consumir el code), para conservarlo. - Montar el code en un
iframeservido desde el exploit server, de modo que el navegador del admin (que siempre tiene una sesión activa en el blog) ejecute la vinculación:
<iframe src="https://0ab500ab03372b7c81abcaf80077006e.web-security-academy.net/oauth-linking?code=2ylpuwjVq2S16PLpGpZEAAfjHHPrmLPcC9giRMoNSy-"></iframe>
- Cuando el admin abre la página del exploit server, el iframe completa la vinculación: el perfil social
peter.wienerqueda adjuntado a la cuenta admin del blog. - Iniciar sesión en el blog vía OAuth con el perfil social
peter.wiener:hotdog→ se accede a la cuenta del admin. - Entrar al panel de administración y ejecutar
DELETEsobrecarlospara resolver el laboratorio.
Root Cause Analysis
La vinculación de perfiles es una operación sensible (une una identidad externa a una cuenta local) y debe estar protegida como cualquier cambio de estado. Aquí el endpoint GET /oauth-linking?code=... no valida que el code corresponda a la sesión que inició el flujo. Al faltar state (o su equivalente de binding session↔code), el code emitido para el atacante se puede ejecutar en el contexto del admin:
// defectuoso: el code no se liga a la sesión iniciadora
// (state nunca fue generado ni verificado en el callback)
code := r.URL.Query().Get("code")
profile, err := oauth.Exchange(code) // code del atacante
linkAccount(currentSession, profile) // vinculado a la sesión admin
Como la vinculación ocurre con la sesión autenticada del admin (vía cookies), el iframe cross-site completa el linking y el atacante obtiene login por OAuth en una cuenta que no controla.
Secure Remediation
- Generar y verificar
stateen el flujo de linking, ligándolo a la sesión del navegador que inició el flujo:
state := newRandomState()
sessions.Set(state, sessionID) // estado ligado a la sesión
// en el callback
if r.URL.Query().Get("state") != state || sessions.Get(state) != sessionID {
return errors.New("state mismatch: flujo no iniciado por esta sesión")
}
- Requerir interacción del usuario en el momento de vincular (doble confirmación, re-autenticación o CAPTCHA) para que el linking forzado no sea silencioso.
- Para el
id_token/codede OpenID Connect, validarnonceyaudademás destate. - Considerar que los endpoints de vinculación en iframe cross-site deben bloquearse (
X-Frame-Options: DENYo CSPframe-ancestors 'none'como capa defensiva adicional, no como único control).