
CVE-2026-49268 — Análise e Remediação de uma Vulnerabilidade de Bypass de Autenticação por Injeção LDAP
| Branch | 1.13-CVE-2026-49268 |
| Autor | Jinwoo Hwang (https://JinwooHwang.com) |
Este branch (1.13-CVE-2026-49268) contém uma remediação de segurança abrangente de uma Vulnerabilidade de Bypass de Autenticação por Injeção LDAP na versão 1.13 do Apache Shiro.
Descrição Oficial do NVD
Um atacante remoto pode injetar caracteres especiais de LDAP na construção do Distinguished Name (DN) na classe DefaultLdapRealm. A entrada de nome de usuário fornecida pelo usuário é concatenada diretamente no template de DN do LDAP sem qualquer escape de caracteres especiais da RFC 2253. Isso permite que um atacante manipule a estrutura do DN usada para autenticação de bind LDAP, potencialmente contornando a autenticação ou se passando por outros usuários. Este problema afeta todas as versões do Apache Shiro até a 2.2.0, e a 3.0.0-alpha-1 ao usar o DefaultLdapRealm.
O Apache Shiro pode autenticar usuários em um diretório LDAP. Para isso, ele transforma um
nome de usuário submetido em um Distinguished Name — o endereço do diretório para a entrada
daquele usuário — inserindo o nome de usuário em um template configurado, por exemplo
uid={0},ou=users,dc=mycompany,dc=com.
Nas versões afetadas, o nome de usuário é colado nesse template como texto bruto. Alguns caracteres de pontuação — mais importante, a vírgula — não são texto comum para um servidor de diretório; eles são a sintaxe que separa uma parte de um endereço da seguinte. Um nome de usuário contendo esses caracteres, portanto, deixa de se comportar como um valor dentro do endereço e passa a se comportar como parte do próprio endereço.
A consequência prática é que uma pessoa que faz login pode influenciar em qual entrada do diretório o Shiro tenta autenticar, em vez de apenas fornecer seu próprio nome. Em vez de ser procurado no contêiner que a implantação pretendia, a busca pode ser redirecionada para outro lugar no diretório. Dependendo de como o diretório está organizado e do que ele permite, isso pode levar à autenticação como a identidade errada, ou à autenticação bem-sucedida quando não deveria.
Perfil de risco. O que aumenta o risco: nenhum acesso prévio ou credencial é necessário — a
entrada chega na fronteira de login, que é acessível por qualquer pessoa que consiga alcançar a
aplicação; e a classe afetada é a forma padrão e documentada de conectar o Shiro ao LDAP,
portanto não é uma configuração exótica. O que reduz o risco: a implantação deve realmente usar
DefaultLdapRealm (ou JndiLdapRealm) com um template de DN configurado; implantações que
autenticam por outros meios, ou que passam um DN completo ou uma credencial não textual como um
certificado, não são afetadas. Se uma busca redirecionada produz uma autenticação utilizável
também depende do próprio layout e das regras de acesso do diretório de destino, que variam por site.
Uma segunda consequência, não relacionada à segurança, vale ser notada para o planejamento de releases: usuários cujos
nomes de usuário legítimos contêm uma barra invertida ou começam com # atualmente não conseguem fazer login
de forma alguma, porque o endereço construído para eles não é um nome bem formado e é rejeitado
de imediato. Outros sinais de pontuação não falham de imediato — eles alteram silenciosamente para qual entrada o
endereço se refere, que é o problema de segurança acima. A mesma correção resolve ambos.
core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java, getUserDn(String),
linhas 227–250 na baseline não corrigida:```java
protected String getUserDn(String principal) throws IllegalArgumentException, IllegalStateException {
if (!StringUtils.hasText(principal)) {
throw new IllegalArgumentException("User principal cannot be null or empty for User DN construction.");
}
String prefix = getUserDnPrefix();
String suffix = getUserDnSuffix();
if (prefix == null && suffix == null) {
log.debug("userDnTemplate property has not been configured, indicating the submitted "
+ "AuthenticationToken's principal is the same as the User DN. Returning the method argument "
+ "as is.");
return principal;
}
int prefixLength = prefix != null ? prefix.length() : 0;
int suffixLength = suffix != null ? suffix.length() : 0;
StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
if (prefixLength > 0) {
sb.append(prefix);
}
sb.append(principal); // <-- inserted verbatim
if (suffixLength > 0) {
sb.append(suffix);
}
return sb.toString();
}
O template é dividido uma vez, no momento da configuração, em torno do token `{0}`
(`setUserDnTemplate`, linhas 181–200), em um `prefix` e um `suffix`. No momento da autenticação
o principal é concatenado entre eles. **Nenhuma codificação é aplicada em momento algum.** Não há
nenhum auxiliar de escape em nenhum lugar de `org/apache/shiro/realm/ldap/`.
### 3.2 A invariante que era assumida mas nunca aplicada
O código ao redor trata o resultado de `getUserDn` como um Distinguished Name bem formado —
ele é passado diretamente para `LdapContextFactory.getLdapContext(...)` e daí para o JNDI como um
bind DN. Isso só é válido se o principal substituído for um *único valor de atributo*.
A concatenação de strings não consegue garantir isso. A RFC 2253 (e a RFC 4514) reservam
`,` `+` `"` `\` `<` `>` `;` `=`, um `#` inicial, e espaços em branco iniciais/finais como sintaxe
estrutural dentro de um DN. Quando qualquer um desses aparece no principal, eles são lidos pelo
parser como estrutura, não como conteúdo. A invariante — *"o principal ocupa exatamente um valor de RDN"* —
era assumida por todos os consumidores a jusante e não era aplicada por nenhum.
### 3.3 Fluxo de execução, do ponto de entrada ao defeito```
submitted credentials (username, password)
│
▼
DefaultLdapRealm.doGetAuthenticationInfo(AuthenticationToken) [line 292]
│
▼
DefaultLdapRealm.getLdapPrincipal(AuthenticationToken) [line 338]
│ principal instanceof String ?
│ ├── no ──► return principal unchanged ── NOT AFFECTED (e.g. X.509)
│ └── yes ──┐
▼ │
DefaultLdapRealm.getUserDn(String) [line 227]
│ prefix == null && suffix == null ?
│ ├── yes ──► return principal unchanged ── NOT AFFECTED ("principal IS the DN")
│ └── no ──┐
▼ │
prefix + principal + suffix ◄── DEFECT: unencoded concatenation [line 246]
│
▼
LdapContextFactory.getLdapContext(userDn, credentials)
│
▼
JNDI bind against the directory using the constructed DN
Template uid={0},ou=users,dc=mycompany,dc=com, principal jsmith,ou=admins:
| DN construído | RDNs analisados | |
|---|---|---|
| Pretendido | uid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com | 4 |
| Baseline (não corrigido) | uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com | 5 |
O nome ganha um componente. ou=users já não é o contentor que está a ser endereçado —
ou=admins é interposto. Verificado por análise com javax.naming.ldap.LdapName, que é
independente de qualquer implementação de escaping.
Comportamento medido do baseline em todo o conjunto reservado (JDK 8, análise LdapName):
Dois caracteres alteram a estrutura do nome, não apenas o seu conteúdo: a vírgula e o
ponto e vírgula — que a RFC 1779 aceita como separador alternativo de RDN. + introduz um segundo
valor de atributo no mesmo RDN. Apenas \ e um # inicial tornam o nome não analisável.
userDnTemplate não definido. getUserDn devolve o principal sem alterações. Este é o
modo documentado "o principal submetido é o DN"; o chamador fornece um DN completo
e é responsável pela sua correção. Não afetado.String. getLdapPrincipal apenas encaminha principais String para
getUserDn; qualquer outra coisa (certificados X.509, tokens personalizados) passa ao lado. Não afetado.setUserDnTemplate (linhas 181–200) rejeita corretamente um template nulo, em branco ou
sem {0}. Valida o template fornecido pelo operador, que nunca foi a entrada
não confiável — portanto é código correto que simplesmente não aborda este defeito.JndiLdapRealm estende DefaultLdapRealm e não substitui getUserDn, pelo que
herda o defeito. Confirmado por teste (§6.1). No âmbito, e corrigido pela mesma alteração.AbstractLdapRealm.searchFilter (linha 89) contém uma segunda substituição {0}, por omissão
(&(objectClass=*)(userPrincipalName={0})), usada no caminho de autorização. Não está afetado.
Uma varredura de todas as chamadas search(, de todos os chamadores de getLdapContext( e de todos os literais
de string com forma de LDAP em core, support e web main sources encontrou exatamente um uso de
searchFilter — ActiveDirectoryRealm.getRoleNamesForUser, linha 172:```java
Object[] searchArguments = new Object[]{userPrincipalName};
NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
Este é o overload **parametrizado** `DirContext.search(String, String, Object[], SearchControls)`.
O nome de usuário é passado como *argumento* de filtro, nunca concatenado na string
do filtro. De acordo com o javadoc do `javax.naming.directory.DirContext` do JDK 8, textualmente:
> "Quando um argumento de filtro com valor de string é substituído por uma variável, o filtro é
> interpretado como se a string fosse fornecida no lugar da variável, com quaisquer caracteres
> que tenham significado especial dentro de filtros (como `'*'`) tendo sido escapados de acordo
> com as regras da RFC 2254."
O provedor JNDI realiza o escaping. O registro histórico disso está na própria
declaração do campo.
**Fonte: `core/src/main/java/org/apache/shiro/realm/ldap/AbstractLdapRealm.java`, linhas 88–89**```java
//SHIRO-115 - prevent potential code injection:
protected String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";
Fonte: core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java, linhas 158–172```java
protected Set getRoleNamesForUser(String username, LdapContext ldapContext) throws NamingException {
Set roleNames;
roleNames = new LinkedHashSet();
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String userPrincipalName = username;
if (principalSuffix != null && !userPrincipalName.toLowerCase(Locale.ROOT).endsWith(principalSuffix.toLowerCase(Locale.ROOT))) {
userPrincipalName += principalSuffix;
}
Object[] searchArguments = new Object[]{userPrincipalName};
NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
O nome de usuário chega ao diretório como `searchArguments[0]`, nunca como texto inserido em
`searchFilter`. O token `{0}` no filtro é resolvido pelo provedor JNDI, não pelo
Shiro.
`ActiveDirectoryRealm` na linha 108 passa o nome de usuário bruto para
`getLdapContext(username, password)`. Esse valor se torna uma única entrada de ambiente
`SECURITY_PRINCIPAL` do JNDI; ele não é substituído em um template e nenhum DN é
construído a partir dele, portanto está fora do mecanismo desta falha.
**Verificado empiricamente contra um servidor de diretório ativo.** O escape do JNDI não foi aceito
com base na confiança: foi observado no tráfego. Um servidor LDAP em processo
(UnboundID `InMemoryDirectoryServer`) foi instrumentado com um
`InMemoryOperationInterceptor` que registra o filtro de cada requisição de busca que recebe,
e o `searchFilter` padrão do Shiro foi emitido através da mesma chamada JNDI parametrizada usada
por `ActiveDirectoryRealm`, com um argumento especialmente construído:```
filter template : (&(objectClass=*)(userPrincipalName={0}))
argument passed : *)(uid=jsmith
server received : (&(objectClass=*)(userPrincipalName=\2a\29\28uid=jsmith))
O *, ) e ( chegaram ao servidor como \2a, \29 e \28 — escapados conforme a
RFC 2254. O filtro retém exatamente duas cláusulas; o argumento não conseguiu adicionar uma terceira.
Controle, o mesmo template com o argumento inserido como texto bruto em vez de passado como um argumento de filtro:``` CONTROL, raw splice: (&(objectClass=)(userPrincipalName=)(uid=jsmith)) server received : (&(objectClass=)(userPrincipalName=)(uid=jsmith))
O controle chega ao servidor com sua estrutura alterada — uma terceira cláusula adicionada e o
teste `userPrincipalName` reduzido a um curinga. A forma parametrizada não. Isso
confirma, por observação e não por contrato, que o caminho `searchFilter` não é
afetado.
---
## 4. Etapas de Reprodução Passo a Passo
Determinístico, a partir de um checkout limpo. O diretório de trabalho é a raiz do repositório em todo o processo.
### 4.1 Pré-requisitos
| Requisito | Valor utilizado |
|---|---|
| JDK | **8** — o branch define `jdk.version` 1.8. Os resultados abaixo foram produzidos com Zulu 1.8.0_432 (arm64). Aponte `JAVA_HOME` para uma instalação do JDK 8 antes de executar qualquer comando nesta seção: |
| Build | Apache Maven, acesso à rede necessário (POM pai `org.apache:apache:38`) |
| Baseline | `origin/1.13.x` |
| Configuração | `userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com` |
| Servidor de diretório | **Não necessário para §4.3-§4.4** — o defeito está na construção do DN, antes de qualquer chamada de rede, portanto essas etapas simulam `LdapContextFactory`. §4.5 adicionalmente reproduz todo o caminho contra um servidor LDAP **real** via HTTP. |
Definindo `JAVA_HOME` para uma instalação do JDK 8:```bash
# macOS
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
# Linux (path varies by distribution and vendor)
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
# Windows (cmd)
set JAVA_HOME=C:\Program Files\Zulu\zulu-8
Confirme com mvn -v, que informa qual JDK o Maven usará. Os comandos abaixo assumem que
isso já está configurado.
git clone https://github.com/apache/shiro.git cd shiro git checkout -b repro origin/1.13.x
### 4.3 Observar o defeito diretamente
Este é o gatilho mínimo, independente da build do Shiro. Escreva `Repro.java` em um diretório
temporário:```java
import javax.naming.ldap.LdapName;
public class Repro {
// reproduces DefaultLdapRealm.getUserDn line-for-line
static String getUserDn(String prefix, String principal, String suffix) {
StringBuilder sb = new StringBuilder(prefix.length() + principal.length() + suffix.length());
sb.append(prefix);
sb.append(principal);
sb.append(suffix);
return sb.toString();
}
public static void main(String[] args) throws Exception {
String prefix = "uid=";
String suffix = ",ou=users,dc=mycompany,dc=com";
String benign = getUserDn(prefix, "jsmith", suffix);
System.out.println("benign : " + benign + " -> RDNs=" + new LdapName(benign).size());
String crafted = getUserDn(prefix, "jsmith,ou=admins", suffix);
System.out.println("crafted: " + crafted + " -> RDNs=" + new LdapName(crafted).size());
}
}
Execute-o:```bash javac Repro.java && java Repro
Saída observada:```
benign : uid=jsmith,ou=users,dc=mycompany,dc=com -> RDNs=4
crafted: uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com -> RDNs=5
A contagem de componentes muda de 4 para 5. O valor submetido tornou-se estrutura de nome.
A partir da raiz do repositório na baseline não corrigida, aplique os testes da §6.3 e execute:```bash mvn -B clean verify
A compilação para em `Apache Shiro :: Core` com os testes de comprovação falhando — consulte §6.1 para a saída completa capturada:```
DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
Reproduzido através da pilha completa — uma requisição HTTP real, um contêiner de servlet real,
o próprio FormAuthenticationFilter do Shiro, e um servidor LDAP real. Nada é simulado em nenhuma
camada.
Fixture de diretório — um contêiner privilegiado aninhado, um layout comum do mundo real:``` dc=mycompany,dc=com └── ou=users ├── uid=jsmith userPassword: userpass (ordinary account) └── ou=admins └── uid=jsmith userPassword: adminpass (privileged account)
#### 4.5.1 Build afetado
**Requisição**```http
POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
username=jsmith%2Cou%3Dadmins&password=adminpass
Resposta```http HTTP/1.1 302 Found Location: http://127.0.0.1/;jsessionid=node0qu3v2n612vy71alsuyir2bgzz1.node0
**Bind DN que o diretório recebeu**```
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
O 302 é o redirecionamento de sucesso do FormAuthenticationFilter: a requisição foi autenticada.
O DN carrega cinco componentes onde o template define quatro, e a entrada alcançada é
a privilegiada sob ou=admins.
Controle — o mesmo nome de usuário com a senha da conta comum```http POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded
username=jsmith%2Cou%3Dadmins&password=userpass
I'll analyze the content and provide the translation. However, I notice that the INPUT section appears to be empty — no actual Markdown content was provided after "INPUT:".
Please provide the actual chunk 39 content to translate, and I'll return only the translated Markdown text following all the rules specified.```http
HTTP/1.1 200 OK
Rejeitado. Isto é o que estabelece a personificação em vez de coincidência: o nome de usuário elaborado autentica com a senha da conta privilegiada e falha com a do usuário comum, portanto as credenciais verificadas pertencem a uma entrada de diretório diferente daquela que os templates endereçam.
Bind DN recebido pelo diretório para o nome de usuário elaborado:``` affected : uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com (5 components) remediated : uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com (4 components)
Logins comuns permanecem inalterados — a linha 1 autentica de forma idêntica em ambas as builds, com o
diretório recebendo `uid=jsmith,ou=users,dc=mycompany,dc=com`.
Ambas as execuções usaram o mesmo harness e o mesmo fixture; apenas o `shiro-core` no classpath
diferiu. A classe efetivamente carregada foi confirmada com `-verbose:class` para cada execução.
---
## 5. Detalhes de Remediação e Código de Remediação
### 5.1 Correção primária
Codifique o principal como um único valor de atributo DN antes da substituição, usando o
próprio codificador RFC 2253 do JDK, `javax.naming.ldap.Rdn.escapeValue`. Este é o mesmo mecanismo que o
JDK usa para construir nomes, portanto sua saída é, por construção, consistente com o parser que
a consome.
**Arquivo:** `core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java````diff
@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
import javax.naming.AuthenticationNotSupportedException;
import javax.naming.NamingException;
import javax.naming.ldap.LdapContext;
+import javax.naming.ldap.Rdn;
/**
* An LDAP {@link org.apache.shiro.realm.Realm Realm} implementation utilizing Sun's/Oracle's
@@ -239,11 +240,14 @@ public class DefaultLdapRealm extends AuthorizingRealm {
int prefixLength = prefix != null ? prefix.length() : 0;
int suffixLength = suffix != null ? suffix.length() : 0;
- StringBuilder sb = new StringBuilder(prefixLength + principal.length() + suffixLength);
+ //the principal is a single attribute value within the resulting name, so it is encoded
+ //to keep any characters that are significant in a Distinguished Name within that value:
+ String value = Rdn.escapeValue(principal);
+ StringBuilder sb = new StringBuilder(prefixLength + value.length() + suffixLength);
if (prefixLength > 0) {
sb.append(prefix);
}
- sb.append(principal);
+ sb.append(value);
if (suffixLength > 0) {
sb.append(suffix);
}
O que o hunk impõe. Rdn.escapeValue escapa com barra invertida o conjunto reservado da RFC 2253
(\ , = + < > # ; ") mais espaços em branco à esquerda e à direita. Depois disso, o texto
substituído só pode ocupar um valor de RDN: o parser lê os caracteres escapados como conteúdo, pelo que
a contagem de componentes do resultado é fixada pelo template e não pode ser influenciada pelo
principal. A dica de capacidade do StringBuilder é atualizada para o comprimento codificado — um detalhe
de dimensionamento, não de comportamento.
Colocação. A codificação fica depois do retorno antecipado para o caso de template não configurado. Isso é deliberado: nesse modo o chamador fornece um DN completo e codificá-lo iria corrompê-lo. Ambos os ramos mantêm os seus contratos existentes.
Esta correção altera a string de DN produzida para qualquer principal que contenha um caractere reservado. Três consequências que vale a pena enunciar claramente:
# produziam um DN que não era um nome bem formado, pelo que nenhum bind podia sequer ser tentado.
Agora codificam para um DN válido e irão tentar um bind. Os sites podem ver contas começarem
a autenticar-se que anteriormente não conseguiam — uma correção, mas uma alteração visível. Nomes de utilizador
que contêm ,, ;, + ou = não eram rejeitados antes; resolviam para a entrada
errada, e agora resolvem para a pretendida.uid=jsmith\,ou\=admins,...). Nenhuma alteração de configuração é necessária.getUserDnTemplate() está implementado como getUserDn("{0}"). { e } não são reservados na
RFC 2253, pelo que o valor de retorno do acessor permanece inalterado. Confirmado pelo
testUserDnTemplate pré-existente, que passa sem modificações.
Ramo 1.13-CVE-2026-49268, JDK Zulu 1.8.0_432. Comando conforme §4.4.```
DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
Saída de falha capturada — esta é a evidência, e não é recuperável depois que a correção estiver aplicada:```
testGetUserDnPreservesTemplateStructure:238
User DN gained or lost components relative to the template:
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
expected:<4> but was:<5>
testGetUserDnPreservesTemplateStructureForReservedCharacters:262
Component count changed for principal [jsmith,ou=admins]:
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
expected:<4> but was:<5>
testGetUserDnEncodesSubstitutedValue:280
expected:<uid=jsmith[\,ou\]=admins,ou=users,dc=...>
but was:<uid=jsmith[,ou]=admins,ou=users,dc=...>
testUserDnTemplateSubstitutionPreservesStructure:300
Unexpected method call
LdapContextFactory.getLdapContext("uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com", ...)
expected:
LdapContextFactory.getLdapContext("uid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com", ...)
expected: 1, actual: 0
testGetUserDnLeavesOrdinaryPrincipalUnchanged passou na linha de base, conforme pretendido — é uma proteção contra correção excessiva, não um teste de comprovação.
Mesmo comando, mesmo JDK:``` DefaultLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
Todos os quatro testes de comprovação agora passam em ambas as classes. Reexecutar §4.3 com a correção aplicada
produz `RDNs=4` para o principal construído.
### 6.3 Testes adicionados
**Arquivo:** `core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java`
(+124 linhas, 0 exclusões). Como `JndiLdapRealmTest extends DefaultLdapRealmTest`, cada
teste abaixo é executado contra **ambos** os realms — 10 execuções a partir de 5 métodos.
| Teste | Verifica | Linha de base |
|---|---|---|
| `testGetUserDnLeavesOrdinaryPrincipalUnchanged` | Um principal comum produz o DN exato esperado, 4 RDNs, valor intacto. Protege contra escape excessivo. | **passou** (guarda) |
| `testGetUserDnPreservesTemplateStructure` | Para `jsmith,ou=admins` o DN ainda tem 4 componentes e o principal sobrevive como um único valor de atributo. | falhou |
| `testGetUserDnPreservesTemplateStructureForReservedCharacters` | O mesmo, em todos os 10 principais com caracteres reservados; também falha o teste se o DN não for um nome bem formado. | falhou |
| `testGetUserDnEncodesSubstitutedValue` | O valor substituído é codificado de forma que faz round-trip para o principal submetido. | falhou |
| `testUserDnTemplateSubstitutionPreservesStructure` | Ponta a ponta através de `getAuthenticationInfo`: o DN entregue a `LdapContextFactory` mantém a estrutura do template. | falhou |
**Nota de design.** As asserções estruturais analisam o resultado com `javax.naming.ldap.LdapName`
e comparam contagens de componentes e o valor folha submetido a round-trip, em vez de afirmar uma
string escapada esperada. Os testes, portanto, verificam a propriedade real exigida e não
pressupõem `Rdn.escapeValue` como a implementação — um codificador correto alternativo
ainda passaria.
Comando:```bash
mvn -B clean verify
O build completo do reactor passa com nenhuma flag e nenhum skip:``` mvn -B clean verify
**BUILD SUCCESS — 907 testes, 0 falhas, 0 erros, 3 ignorados, em todo o reactor.**
Todos os gates estão ativos nesta execução: testes unitários (surefire), testes de integração (failsafe),
a auditoria de licenças Apache RAT, maven-enforcer e japicmp.
| Gate | Resultado |
|---|---|
| Testes unitários, reactor completo | **907 executados, 0 falhas, 0 erros, 3 ignorados** |
| Módulo `core` isoladamente | **321 executados, 0 falhas, 0 erros, 0 ignorados** |
| Testes LDAP (`DefaultLdapRealmTest` + `JndiLdapRealmTest`) | **32 executados, 0 falhas** |
| Testes de integração (failsafe) | executados em todo o reactor, sem falhas |
| Auditoria de licenças Apache RAT | Não aprovadas: 0, desconhecidas: 0 |
| maven-enforcer | sem violações |
| japicmp | nenhuma incompatibilidade reportada |
Os 3 ignorados são `@Ignore`s pré-existentes em módulos não relacionados a esta alteração; `core` — o
único módulo afetado — não ignora nada.
## Referências
- Registo CVE (MITRE API): `https://cveawg.mitre.org/api/cve/CVE-2026-49268`
- Anúncio upstream: `https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s`
- Relatórios de segurança do Apache Shiro: `https://shiro.apache.org/security-reports.html`
- RFC 2253 / RFC 4514 — representação em string LDAP de Distinguished Names
- RFC 4515 — representação em string de filtros de pesquisa LDAP
## Contacto
Autor de Investigação e Remediação de Vulnerabilidades de Segurança: Jinwoo Hwang ([https://JinwooHwang.com](https://jinwoohwang.com/))
| Campo | Valor |
|---|
| CVE | CVE-2026-49268 — Apache Shiro: Injeção de DN LDAP no DefaultLdapRealm |
| CWE ID | CWE-90 — Neutralização Incorreta de Elementos Especiais usados em uma Consulta LDAP ('Injeção LDAP') |
| CVSS v4.0 | 8.8 ALTO — CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N/S:P/AU:Y/R:A/RE:L/U:Red |
| CVSS v3.1 | 9.1 CRÍTICO — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Versões afetadas | org.apache.shiro:shiro-core 0 até 2.2.0 inclusive; 3.0.0-alpha-0 até 3.0.0-alpha-1 inclusive |
| Corrigido upstream em | 2.2.1 e 3.0.0-alpha-2 |
| Referência | https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s |
| Principal | Resultado do baseline | Efeito |
|---|
jsmith,ou=admins | analisa, 5 RDNs | estrutura alterada — um contentor é interposto |
jsmith;ou=admins | analisa, 5 RDNs | estrutura alterada — ; é um separador de RDN segundo a RFC 1779 |
jsmith+uid=admin | analisa, 4 RDNs | torna-se um RDN multi-valor; uid ganha um segundo valor |
jsmith=admin | analisa, 4 RDNs | valor corrompido |
quo"te, angle<br>ackets, leadingSpace, trailingSpace | analisam, 4 RDNs | valor corrompido |
back\slash | IllegalArgumentException | malformado — o bind não pode ser tentado |
#leadingNumberSign | IllegalArgumentException | malformado — o bind não pode ser tentado |
| Camada | Componente |
|---|
| Cliente HTTP | HttpURLConnection, POST de formulário bruto |
| Contêiner de servlet | Embedded Jetty 9.4.58.v20250814 |
| Filtro de segurança | ShiroFilter + EnvironmentLoaderListener, authc (FormAuthenticationFilter) |
| Realm | DefaultLdapRealm, userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com |
| Diretório | UnboundID InMemoryDirectoryServer, instrumentado para registrar cada bind DN |
| Requisição | Afetado | Corrigido |
|---|
username=jsmith&password=userpass | 302 autenticado | 302 autenticado |
username=jsmith%2Cou%3Dadmins&password=adminpass | 302 autenticado | 200 rejeitado |
username=jsmith%2Cou%3Dadmins&password=userpass | 200 rejeitado | 200 rejeitado |