
Persistencia de sesión de listmonk después del restablecimiento y cambio de contraseña
Persistencia de sesión de listmonk tras el restablecimiento y cambio de contraseña
Encontré este problema al revisar listmonk, un gestor de boletines y listas de correo de código abierto, con una simple pregunta de seguridad en mente:
Cuando un usuario cambia o restablece una contraseña, ¿la aplicación realmente termina las sesiones ya emitidas?
En este caso, la respuesta fue no.
Las sesiones autenticadas emitidas anteriormente siguieron siendo válidas después de:
Eso significaba que una cookie de sesión robada podía sobrevivir a los mismos eventos de seguridad en los que los usuarios confían para recuperar su cuenta.
El problema fue aceptado y se le asignó CVE-2026-34828.
Proyecto: listmonk en GitHub
CVE: CVE-2026-34828
Esto afectó a listmonk, un proyecto ampliamente adoptado con más de 5M de pulls de Docker.
sesión autenticada robada → la víctima restablece o cambia la contraseña → la sesión antigua sigue siendo válida → el atacante conserva el acceso a la cuenta tras la recuperación de credenciales
listmonk es un gestor de listas de correo y boletines autohospedado.
Ofrece:
Eso significa que su modelo de sesión es un límite de seguridad real.
La pregunta importante aquí no era si listmonk soporta el restablecimiento de contraseña.
La pregunta real era:
¿El restablecimiento o el cambio de contraseña revocan realmente la persistencia del atacante si ya se ha robado una sesión?
En este caso, no lo hacía.
Muchas revisiones de seguridad se centran demasiado en los bypass de inicio de sesión y en la escalada de privilegios obvia.
Eso pasa por alto una clase importante de debilidades:
fallos de recuperación
Si un usuario cambia o restablece una contraseña, se supone que esa acción debe ser significativa.
Se supone que debe reducir la confianza en credenciales antiguas y en el estado de autenticación previo.
Si un atacante ya tiene una sesión válida y esa sesión sobrevive al evento de recuperación, entonces la víctima no ha recuperado realmente la cuenta por completo.
Ese era el problema aquí.
Esto no era un fallo de validación de inicio de sesión. No era un problema de criptografía. No era un fallo en el hash de contraseñas.
Era un fallo del ciclo de vida de la sesión:
Eso es suficiente para crear una vulnerabilidad real.
No abordé listmonk golpeando endpoints al azar esperando que alguno cediera.
El camino más sólido era identificar primero el límite de confianza de mayor valor.
Para software con un fuerte componente de autenticación, uno de los mejores límites para probar es este:
¿Los cambios de cuenta sensibles para la seguridad revocan las sesiones previamente confiables?
Esa pregunta suele volverse interesante en torno a:
En listmonk, la señal más fuerte provino de los dos primeros.
Ahí es donde el problema se volvió evidente.
El fallo no era que los cambios de contraseña fallaran.
El fallo era que las sesiones les sobrevivían.
Según la revisión del código fuente, el flujo de restablecimiento de contraseña:
pero no había una revocación visible de las sesiones anteriores.
El mismo patrón aparecía en el flujo autenticado de cambio de contraseña:
Ese comportamiento coincidía exactamente con los resultados en vivo.
Las áreas de código relevantes que revisé fueron:
cmd/auth.go para el comportamiento de olvido/restablecimientocmd/users.go para actualizaciones autenticadas del perfilinternal/core/users.go para el manejo de la actualización de contraseñaPorque el robo de sesión es una condición de ataque real.
Una vez que un atacante obtiene una cookie de sesión autenticada válida por cualquier medio, como:
la víctima debería poder terminar esa persistencia del atacante cambiando o restableciendo la contraseña.
Aquí, no podía.
La cadena de ataque era sencilla:
Esa es toda la vulnerabilidad.
La distinción importante es la persistencia después de la recuperación.
Muchas aplicaciones tratan el cambio de contraseña como un evento puramente a nivel de credenciales. Eso no es suficiente.
La pregunta real no es:
"¿Cambió el valor de la contraseña en el almacenamiento?"
La pregunta real es:
"¿Se revocó la relación de confianza asociada a las sesiones antiguas?"
En listmonk, no se revocó.
Eso convierte lo que podría haber sido un mantenimiento ordinario de la cuenta en una recuperación de seguridad incompleta.
Esa es la diferencia entre:
Validé el problema en dos flujos separados.
Primero, creé un usuario de prueba normal e inicié sesión, guardando la cookie de sesión autenticada.
Luego activé el flujo de contraseña olvidada, capturé el enlace de restablecimiento y restablecí la contraseña.
Después del restablecimiento:
Una solicitud de validación representativa tenía este aspecto:
GET /api/profile HTTP/1.1
Host: 127.0.0.1:9000
Cookie: session=<old_pre_reset_session>
Y el servidor seguía devolviendo:
HTTP/1.1 200 OK
Content-Type: application/json
con el perfil autenticado.
Eso confirmó la afirmación principal:
Luego validé la misma clase de fallo en el flujo autenticado de cambio de contraseña.
Inicié sesión dos veces como el mismo usuario y guardé dos sesiones autenticadas válidas:
Usando la sesión A, cambié la contraseña a través del endpoint de actualización de perfil.
Ejemplo de solicitud:
PUT /api/profile HTTP/1.1
Host: 127.0.0.1:9000
Cookie: session=<session_A>
Content-Type: application/json
{
"name":"victim1",
"email":"[email protected]",
"password":"VictimChanged123"
}
Después de eso:
Una solicitud de seguimiento que usaba la sesión B seguía devolviendo datos autenticados de /api/profile.
Eso demostró que el problema no se limitaba a la ruta de olvido/restablecimiento.
También afectaba a los cambios de contraseña autenticados normales.
Una sola reproducción ya habría sido suficiente para mostrar un problema.
Pero validar ambos flujos importaba por dos razones.
Mostró que el fallo no estaba aislado a una única ruta de recuperación en casos límite.
La misma propiedad de seguridad fallaba en:
Hizo que el problema fuera más difícil de descartar como lógica de negocio accidental.
Esto era claramente una debilidad más amplia de la gestión de sesiones:
Eso le dio al problema un peso de seguridad mucho mayor.
También probé el flujo de restablecimiento en una cuenta con TOTP habilitado porque quería saber si el restablecimiento de contraseña debilitaría silenciosamente o eludiría las expectativas de 2FA.
Lo que confirmé fue:
Esa fue una comprobación de límites útil.
Acotó el problema correctamente.
La vulnerabilidad no era:
El problema real seguía siendo:
Ese es un hallazgo más limpio y defendible.
Este problema se clasificó razonablemente como de severidad Alta.
El impacto clave aquí es el acceso no autorizado persistente después de las acciones de recuperación de seguridad de la cuenta.
La clasificación del aviso fue:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:NEso tiene sentido.
La afirmación no es que un atacante pueda iniciar sesión sin credenciales de la nada. La afirmación es que, una vez que un atacante ha obtenido una sesión autenticada válida, la víctima no puede terminar por completo ese acceso realizando las mismas acciones de seguridad que se supone que deben recuperar la cuenta, es decir, el restablecimiento y el cambio de contraseña.
Esa es una vulnerabilidad real y defendible de gestión de sesiones.
Algunas personas subestiman los fallos de persistencia de sesión porque asumen que el robo de sesión ya es "fin del juego".
Eso es demasiado simplista.
La pregunta real es qué sucede después de que la víctima nota que algo está mal y actúa.
Si:
entonces la recuperación de la cuenta es incompleta.
Eso no es solo un comportamiento incómodo. Es un fallo de seguridad en el modelo de recuperación.
Especialmente en una plataforma orientada a administradores, es un problema significativo con un fuerte impacto en la confidencialidad.
El mantenedor corrigió el problema en el commit:
db82035
El enfoque central de la corrección es exactamente lo que este fallo necesitaba:
Esa es la remediación correcta porque apunta a la propiedad de seguridad real que falló:
la confianza antigua debería morir cuando cambian las credenciales
Una buena corrección para esta clase de fallo no se trata de cambiar la validación de contraseñas. Se trata de revocar el estado de sesión previamente activo asociado a la cuenta.
Esa es la parte que restaura la recuperación real.
Este problema se reportó de forma privada a través del flujo de reporte de seguridad de GitHub.
El mantenedor:
CVE-2026-34828
Una cosa que surgió durante el manejo del aviso fue el alcance.
El reporte original incluía ambos:
GitHub inicialmente trató estos como problemas corregibles de forma independiente a efectos de asignación de CVE. Es un recordatorio útil de que el alcance del aviso importa incluso cuando la debilidad subyacente es conceptualmente similar.
El resultado final fue CVE-2026-34828.
La lección clave aquí es simple:
cambiar las credenciales no es suficiente si la confianza autenticada antigua sigue viva.
Muchos desarrolladores piensan en términos de:
Esas cosas importan.
Pero el límite de seguridad real es más amplio:
cuando ocurre un evento de cuenta de alto riesgo, ¿qué estado previamente confiable debe dejar de ser confiable?
En este caso, la respuesta debería haber sido:
Y listmonk no estaba haciendo eso.
Esa es la verdadera conclusión.
Esta vulnerabilidad no se trataba de payloads sofisticados ni de trucos ingeniosos de parser.
Se trataba de hacer la pregunta correcta sobre el límite de confianza.
En listmonk, la contraseña cambió.
La acción de recuperación se completó.
Pero la sesión antigua del atacante seguía viva.
Por eso esto se convirtió en CVE-2026-34828.
