Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
shiro — CVE-2026-49268 — Análise e Remediação de uma Vulnerabilidade de Bypass de Autenticação por Injeção LDAP | Kitploit
Ferramentas/GitHubGitHub/sassoftware/shiro
Análise Estática de Código (SAST)Análise de VulnerabilidadesAnálise de CódigoSegurança WebAutenticaçãoAprendizado e Educação
GitHubsassoftware/shiro

shiro

CVE-2026-49268 — Análise e Remediação de uma Vulnerabilidade de Bypass de Autenticação por Injeção LDAP

Ver Repositório
1há 6 diasAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2026-49268 — Análise e Remediação de uma Vulnerabilidade de Bypass de Autenticação por Injeção LDAP

Branch1.13-CVE-2026-49268
AutorJinwoo 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.


1. Visão Geral da Vulnerabilidade


2. Resumo Executivo

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.


3. Análise da Causa Raiz

3.1 O defeito

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; }

root@kitploit:~
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();

}

root@kitploit:~
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

3.4 Efeito demonstrado

Template uid={0},ou=users,dc=mycompany,dc=com, principal jsmith,ou=admins:

DN construídoRDNs analisados
Pretendidouid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com4
Baseline (não corrigido)uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com5

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.

3.5 Código adjacente que está correto, e porquê — raio de impacto

  • 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.
  • Principais não-String. getLdapPrincipal apenas encaminha principais String para getUserDn; qualquer outra coisa (certificados X.509, tokens personalizados) passa ao lado. Não afetado.
  • Validação de 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.

3.6 Caminho relacionado — AVALIADO, NÃO AFETADO

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);

root@kitploit:~
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();

root@kitploit:~
    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);
root@kitploit:~
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))

root@kitploit:~
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.

4.2 Obter a baseline não corrigida```bash

git clone https://github.com/apache/shiro.git cd shiro git checkout -b repro origin/1.13.x

root@kitploit:~
### 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

root@kitploit:~
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.

4.4 Reproduzir através da própria API do Shiro

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

root@kitploit:~
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

4.5 Reprodução ponta a ponta via HTTP contra um servidor de diretório ativo

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)

root@kitploit:~
#### 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

root@kitploit:~
**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

root@kitploit:~
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.

4.5.2 Build corrigido — requisições idênticas

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)

root@kitploit:~
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.

5.2 Alteração de comportamento para chamadores legítimos

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:

  1. Inícios de sessão anteriormente quebrados agora funcionam. Nomes de utilizador que contêm uma barra invertida ou que começam com # 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.
  2. Os DNs de bind enviados para o diretório diferem para os nomes de utilizador afetados. Registos do lado do diretório, trilhas de auditoria, e qualquer recolha de registos que corresponda a strings de DN literais verão formas escapadas (uid=jsmith\,ou\=admins,...). Nenhuma alteração de configuração é necessária.
  3. Implementações que dependiam do comportamento antigo para construir DNs multi-componente a partir do campo de nome de utilizador iriam quebrar. Esta não é uma configuração suportada — o template existe para definir estrutura — mas é o único risco de migração, e é o mesmo comportamento que a correção existe para remover.

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.


6. Verificação e Testes

6.1 Antes — linha de base, sem correção

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

root@kitploit:~
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.

6.2 Depois — com a correção aplicada

Mesmo comando, mesmo JDK:``` DefaultLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0

root@kitploit:~
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

6.4 Regressão — build completo

O build completo do reactor passa com nenhuma flag e nenhum skip:``` mvn -B clean verify

root@kitploit:~
**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/))
Baixar ferramenta
CampoValor
CVECVE-2026-49268 — Apache Shiro: Injeção de DN LDAP no DefaultLdapRealm
CWE IDCWE-90 — Neutralização Incorreta de Elementos Especiais usados em uma Consulta LDAP ('Injeção LDAP')
CVSS v4.08.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.19.1 CRÍTICO — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Versões afetadasorg.apache.shiro:shiro-core 0 até 2.2.0 inclusive; 3.0.0-alpha-0 até 3.0.0-alpha-1 inclusive
Corrigido upstream em2.2.1 e 3.0.0-alpha-2
Referênciahttps://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s
PrincipalResultado do baselineEfeito
jsmith,ou=adminsanalisa, 5 RDNsestrutura alterada — um contentor é interposto
jsmith;ou=adminsanalisa, 5 RDNsestrutura alterada — ; é um separador de RDN segundo a RFC 1779
jsmith+uid=adminanalisa, 4 RDNstorna-se um RDN multi-valor; uid ganha um segundo valor
jsmith=adminanalisa, 4 RDNsvalor corrompido
quo"te, angle<br>ackets, leadingSpace, trailingSpace analisam, 4 RDNsvalor corrompido
back\slashIllegalArgumentExceptionmalformado — o bind não pode ser tentado
#leadingNumberSignIllegalArgumentExceptionmalformado — o bind não pode ser tentado
CamadaComponente
Cliente HTTPHttpURLConnection, POST de formulário bruto
Contêiner de servletEmbedded Jetty 9.4.58.v20250814
Filtro de segurançaShiroFilter + EnvironmentLoaderListener, authc (FormAuthenticationFilter)
RealmDefaultLdapRealm, userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com
DiretórioUnboundID InMemoryDirectoryServer, instrumentado para registrar cada bind DN
RequisiçãoAfetadoCorrigido
username=jsmith&password=userpass302 autenticado302 autenticado
username=jsmith%2Cou%3Dadmins&password=adminpass302 autenticado200 rejeitado
username=jsmith%2Cou%3Dadmins&password=userpass200 rejeitado200 rejeitado