
CVE-2026-49268 — Analisi e correzione di una vulnerabilità di bypass dell'autenticazione tramite LDAP Injection
| Branch | 1.13-CVE-2026-49268 |
| Autore | Jinwoo Hwang (https://JinwooHwang.com) |
Questo branch (1.13-CVE-2026-49268) contiene una remediation di sicurezza completa di una Vulnerabilità di LDAP Injection Authentication Bypass in Apache Shiro versione 1.13.
Descrizione ufficiale NVD
Un attaccante remoto può iniettare caratteri speciali LDAP nella costruzione del Distinguished Name (DN) nella classe DefaultLdapRealm. L'input del nome utente fornito dall'utente viene concatenato direttamente nel template LDAP DN senza alcun escaping dei caratteri speciali RFC 2253. Ciò consente a un attaccante di manipolare la struttura del DN utilizzata per l'autenticazione bind LDAP, potenzialmente aggirando l'autenticazione o impersonando altri utenti. Questo problema riguarda tutte le versioni di Apache Shiro fino alla 2.2.0, e 3.0.0-alpha-1 quando si utilizza DefaultLdapRealm.
Apache Shiro può autenticare gli utenti su una directory LDAP. Per farlo, trasforma un
nome utente inviato in un Distinguished Name — l'indirizzo della directory per l'entry di quell'utente
— inserendo il nome utente in un template configurato, ad esempio
uid={0},ou=users,dc=mycompany,dc=com.
Nelle versioni interessate il nome utente viene incollato in quel template come testo grezzo. Alcuni caratteri di punteggiatura — soprattutto la virgola — non sono testo ordinario per un directory server; sono la sintassi che separa una parte di un indirizzo dalla successiva. Un nome utente che contiene quei caratteri smette quindi di comportarsi come un valore all'interno dell'indirizzo e inizia a comportarsi come parte dell'indirizzo stesso.
La conseguenza pratica è che una persona che effettua il login può influenzare quale entry della directory Shiro tenta di autenticare, invece di fornire soltanto il proprio nome. Anziché essere cercata nel container previsto dal deployment, la ricerca può essere reindirizzata altrove nella directory. A seconda di come è strutturata la directory e di cosa permette, ciò può portare ad autenticarsi con l'identità sbagliata, o a un'autenticazione che riesce quando non dovrebbe.
Profilo di rischio. Ciò che aumenta il rischio: non sono richiesti accesso o credenziali precedenti — l'input
arriva al confine di login, che è raggiungibile da chiunque possa raggiungere
l'applicazione; e la classe interessata è il modo standard e documentato per collegare Shiro a LDAP,
quindi non si tratta di una configurazione esotica. Ciò che lo riduce: il deployment deve effettivamente utilizzare
DefaultLdapRealm (o JndiLdapRealm) con un template DN configurato; i deployment che
si autenticano con altri mezzi, o che passano un DN completo o una credenziale non testuale come un
certificato, non sono interessati. Se una ricerca reindirizzata produca un'autenticazione utilizzabile
dipende anche dalla struttura e dalle regole di accesso della directory di destinazione, che variano da sito a sito.
Una seconda conseguenza, non di sicurezza, merita di essere notata per la pianificazione del rilascio: gli utenti i cui
nomi utente legittimi contengono un backslash o iniziano con # attualmente non possono effettuare il login
affatto, perché l'indirizzo costruito per loro non è un nome ben formato e viene rifiutato
direttamente. Altra punteggiatura non fallisce direttamente — cambia silenziosamente a quale entry
si riferisce l'indirizzo, che è il problema di sicurezza di cui sopra. La stessa correzione risolve entrambi.
core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java, getUserDn(String),
righe 227–250 sulla baseline non corretta:```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();
}
Il template viene suddiviso una volta, in fase di configurazione, attorno al token `{0}`
(`setUserDnTemplate`, righe 181–200), in un `prefix` e un `suffix`. Al momento dell'autenticazione
il principal viene concatenato tra di essi. **Non viene applicata alcuna codifica in nessun punto.** Non esiste
alcun helper di escaping da nessuna parte in `org/apache/shiro/realm/ldap/`.
### 3.2 L'invariante che era assunto ma mai applicato
Il codice circostante tratta il risultato di `getUserDn` come un Distinguished Name ben formato —
viene passato direttamente a `LdapContextFactory.getLdapContext(...)` e da lì a JNDI come
bind DN. Ciò è corretto solo se il principal sostituito è un *singolo valore di attributo*.
La concatenazione di stringhe non può garantirlo. RFC 2253 (e RFC 4514) riservano
`,` `+` `"` `\` `<` `>` `;` `=`, un `#` iniziale e gli spazi bianchi iniziali/finali come sintassi
strutturale all'interno di un DN. Quando uno qualsiasi di questi caratteri appare nel principal, essi vengono letti dal parser come
struttura, non come contenuto. L'invariante — *"il principal occupa esattamente un valore RDN"* —
era assunto da ogni consumatore a valle e applicato da nessuno.
### 3.3 Flusso di esecuzione, dal punto di ingresso al difetto```
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 costruito | RDN analizzati | |
|---|---|---|
| Previsto | uid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com | 4 |
| Baseline (non corretto) | uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com | 5 |
Il nome acquisisce un componente. ou=users non è più il contenitore a cui ci si rivolge —
ou=admins viene interposto. Verificato tramite parsing con javax.naming.ldap.LdapName, che è
indipendente da qualsiasi implementazione di escaping.
Comportamento misurato della baseline sull'insieme riservato (JDK 8, parsing LdapName):
Due caratteri alterano la struttura del nome, non solo il suo contenuto: la virgola e il
punto e virgola — che RFC 1779 accetta come separatore RDN alternativo. + introduce un secondo
valore di attributo nello stesso RDN. Solo \ e un # iniziale rendono il nome non analizzabile.
userDnTemplate non impostato. getUserDn restituisce il principal invariato. Questa è la
modalità documentata "il principal inviato è il DN"; il chiamante fornisce un DN completo
e ne possiede la correttezza. Non interessato.String. getLdapPrincipal instrada solo i principal String verso
getUserDn; qualsiasi altro (certificati X.509, token personalizzati) passa oltre. Non interessato.setUserDnTemplate (righe 181–200) rifiuta correttamente un template nullo, vuoto o
privo di {0}. Valida il template fornito dall'operatore, che non è mai stato l'input
non attendibile — quindi è codice corretto che semplicemente non affronta questo difetto.JndiLdapRealm estende DefaultLdapRealm e non sovrascrive getUserDn, quindi
eredita il difetto. Confermato dal test (§6.1). Nel perimetro, e corretto dalla stessa modifica.AbstractLdapRealm.searchFilter (riga 89) contiene una seconda sostituzione {0}, predefinita
(&(objectClass=*)(userPrincipalName={0})), usata sul percorso di autorizzazione. Non è interessato.
Un'analisi di ogni chiamata search(, di ogni chiamante di getLdapContext( e di ogni letterale
di stringa in forma LDAP nei sorgenti principali di core, support e web ha trovato esattamente un uso di
searchFilter — ActiveDirectoryRealm.getRoleNamesForUser, riga 172:```java
Object[] searchArguments = new Object[]{userPrincipalName};
NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
Questo è l'overload **parametrizzato** `DirContext.search(String, String, Object[], SearchControls)`.
Il nome utente viene passato come *argomento* del filtro, mai concatenato nella stringa
del filtro. Secondo il javadoc di `javax.naming.directory.DirContext` del JDK 8, testualmente:
> "Quando un argomento del filtro con valore stringa viene sostituito a una variabile, il filtro viene
> interpretato come se la stringa fosse stata fornita al posto della variabile, con qualsiasi carattere
> avente significato speciale all'interno dei filtri (come `'*'`) opportunamente sottoposto a escape secondo
> le regole della RFC 2254."
Il provider JNDI esegue l'escaping. La traccia storica di ciò è nella dichiarazione
del campo stesso.
**Fonte: `core/src/main/java/org/apache/shiro/realm/ldap/AbstractLdapRealm.java`, righe 88–89**```java
//SHIRO-115 - prevent potential code injection:
protected String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";
Source: core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java, righe 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);
L'username raggiunge la directory come `searchArguments[0]`, mai come testo inserito in
`searchFilter`. Il token `{0}` nel filtro viene risolto dal provider JNDI, non da
Shiro.
`ActiveDirectoryRealm` alla riga 108 passa l'username grezzo a
`getLdapContext(username, password)`. Quel valore diventa una singola voce d'ambiente JNDI
`SECURITY_PRINCIPAL`; non viene sostituito in un template e nessun DN viene
costruito da esso, quindi è al di fuori del meccanismo di questo difetto.
**Verificato empiricamente contro un server directory attivo.** L'escaping JNDI non è stato preso
per buono: è stato osservato sul filo. Un server LDAP in-process
(UnboundID `InMemoryDirectoryServer`) è stato strumentato con un
`InMemoryOperationInterceptor` che registra il filtro di ogni richiesta di ricerca che riceve,
e il `searchFilter` predefinito di Shiro è stato emesso attraverso la stessa chiamata JNDI parametrizzata usata
da `ActiveDirectoryRealm`, con un argomento appositamente costruito:```
filter template : (&(objectClass=*)(userPrincipalName={0}))
argument passed : *)(uid=jsmith
server received : (&(objectClass=*)(userPrincipalName=\2a\29\28uid=jsmith))
Il *, ) e ( sono arrivati al server come \2a, \29 e \28 — sottoposti a escape secondo
la RFC 2254. Il filtro mantiene esattamente due clausole; l'argomento non ha potuto aggiungerne una terza.
Controllo, lo stesso template con l'argomento inserito come testo grezzo invece che passato come argomento del filtro:``` CONTROL, raw splice: (&(objectClass=)(userPrincipalName=)(uid=jsmith)) server received : (&(objectClass=)(userPrincipalName=)(uid=jsmith))
Il controllo raggiunge il server con la sua struttura alterata — una terza clausola aggiunta e il
test `userPrincipalName` ridotto a un carattere jolly. La forma parametrizzata no. Questo
conferma, per osservazione anziché per contratto, che il percorso `searchFilter` non è
interessato.
---
## 4. Passaggi di riproduzione dettagliati
Deterministici, da un checkout pulito. La directory di lavoro è la radice del repository per tutto il tempo.
### 4.1 Prerequisiti
| Requisito | Valore utilizzato |
|---|---|
| JDK | **8** — il branch imposta `jdk.version` 1.8. I risultati seguenti sono stati prodotti con Zulu 1.8.0_432 (arm64). Puntare `JAVA_HOME` a un'installazione JDK 8 prima di eseguire qualsiasi comando in questa sezione: |
| Build | Apache Maven, accesso alla rete richiesto (parent POM `org.apache:apache:38`) |
| Baseline | `origin/1.13.x` |
| Configurazione | `userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com` |
| Directory server | **Non richiesto per §4.3-§4.4** — il difetto è nella costruzione del DN, prima di qualsiasi chiamata di rete, quindi quei passaggi simulano `LdapContextFactory`. §4.5 riproduce inoltre l'intero percorso contro un server LDAP **reale** su HTTP. |
Impostazione di `JAVA_HOME` su un'installazione 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
Conferma con mvn -v, che riporta il JDK che Maven utilizzerà. I comandi seguenti presuppongono che
questo sia già impostato.
git clone https://github.com/apache/shiro.git cd shiro git checkout -b repro origin/1.13.x
### 4.3 Osservare direttamente il difetto
Questo è il trigger minimo, indipendente dalla build di Shiro. Scrivi `Repro.java` in una directory
temporanea:```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());
}
}
Eseguilo:```bash javac Repro.java && java Repro
Output osservato:```
benign : uid=jsmith,ou=users,dc=mycompany,dc=com -> RDNs=4
crafted: uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com -> RDNs=5
Il conteggio dei componenti cambia da 4 a 5. Il valore inviato è diventato una struttura di nome.
Dalla radice del repository sulla baseline non corretta, applicare i test nella §6.3 ed eseguire:```bash mvn -B clean verify
La build si interrompe su `Apache Shiro :: Core` con i test di verifica che falliscono — vedi §6.1 per
l'output completo catturato:```
DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
Riprodotto attraverso l'intero stack — una vera richiesta HTTP, un vero servlet container,
il FormAuthenticationFilter di Shiro stesso, e un vero server LDAP. Nulla è mockato a nessun
livello.
Fixture della directory — un container privilegiato annidato, un layout comune nel mondo reale:``` dc=mycompany,dc=com └── ou=users ├── uid=jsmith userPassword: userpass (ordinary account) └── ou=admins └── uid=jsmith userPassword: adminpass (privileged account)
#### 4.5.1 Build interessato
**Richiesta**```http
POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
username=jsmith%2Cou%3Dadmins&password=adminpass
Risposta```http HTTP/1.1 302 Found Location: http://127.0.0.1/;jsessionid=node0qu3v2n612vy71alsuyir2bgzz1.node0
**Bind DN ricevuto dalla directory**```
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
Il 302 è il redirect di successo di FormAuthenticationFilter: la richiesta è stata autenticata.
Il DN contiene cinque componenti dove il template ne definisce quattro, e la voce raggiunta è
quella privilegiata sotto ou=admins.
Controllo — lo stesso username con la password dell'account ordinario```http POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded
username=jsmith%2Cou%3Dadmins&password=userpass
I cannot translate this content because no text was provided. The INPUT section is empty.
Please send the actual Markdown content for chunk 39 of 55, and I will translate it from English to Italian following all the rules you specified.```http
HTTP/1.1 200 OK
Rifiutato. Questo è ciò che stabilisce l'impersonificazione anziché una coincidenza: il username creato ad hoc si autentica con la password dell'account privilegiato e fallisce con quella dell'utente ordinario, quindi le credenziali verificate appartengono a una voce di directory diversa da quella a cui puntano i template.
Bind DN ricevuto dalla directory per il username creato ad hoc:``` 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)
I login ordinari sono invariati — la riga 1 si autentica in modo identico su entrambe le build, con la
directory che riceve `uid=jsmith,ou=users,dc=mycompany,dc=com`.
Entrambe le esecuzioni hanno utilizzato lo stesso harness e la stessa fixture; solo `shiro-core` nel classpath
differiva. La classe effettivamente caricata è stata confermata con `-verbose:class` per ciascuna esecuzione.
---
## 5. Dettagli di remediation e codice di remediation
### 5.1 Correzione primaria
Codificare il principal come un singolo valore di attributo DN prima della sostituzione, utilizzando
l'encoder RFC 2253 del JDK stesso, `javax.naming.ldap.Rdn.escapeValue`. Questo è lo stesso meccanismo che
il JDK utilizza per costruire i nomi, quindi il suo output è per costruzione coerente con il parser che
lo consuma.
**File:** `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);
}
Cosa impone il blocco. Rdn.escapeValue esegue l'escape con backslash del set riservato RFC 2253
(\ , = + < > # ; ") più gli spazi bianchi iniziali e finali. Dopo di esso, il testo sostituito
può occupare solo un valore RDN: il parser legge i caratteri con escape come contenuto, quindi
il numero di componenti del risultato è fissato dal template e non può essere influenzato dal
principal. Il suggerimento di capacità del StringBuilder viene aggiornato alla lunghezza codificata — un dettaglio di dimensionamento, non di comportamento.
Posizionamento. La codifica si trova dopo il ritorno anticipato per il caso di template non configurato. Ciò è deliberato: in quella modalità il chiamante fornisce un DN completo e codificarlo lo corromperebbe. Entrambi i rami mantengono i loro contratti esistenti.
Questa correzione modifica la stringa DN prodotta per qualsiasi principal contenente un carattere riservato. Tre conseguenze che vale la pena enunciare chiaramente:
# producevano un DN che non era un nome ben formato, quindi non era possibile tentare alcun bind.
Ora vengono codificati in un DN valido e tenteranno un bind. I siti potrebbero vedere account che iniziano
ad autenticarsi quando prima non potevano — una correzione, ma un cambiamento visibile. I nomi utente
contenenti ,, ;, + o = non venivano rifiutati prima; risolvevano alla voce
sbagliata, e ora risolvono a quella prevista.uid=jsmith\,ou\=admins,...). Non è richiesta alcuna modifica di configurazione.getUserDnTemplate() è implementato come getUserDn("{0}"). { e } non sono riservati in
RFC 2253, quindi il valore di ritorno dell'accessor è invariato. Confermato dal preesistente
testUserDnTemplate, che passa senza modifiche.
Branch 1.13-CVE-2026-49268, JDK Zulu 1.8.0_432. Comando come da §4.4.```
DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
Output di errore catturato — questa è la prova, e non è recuperabile una volta che la correzione è in atto:```
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 superato sulla baseline, come previsto — è
una protezione contro la correzione eccessiva, non un test di verifica.
Stesso comando, stesso JDK:``` DefaultLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 JndiLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
Tutti e quattro i test di verifica ora passano su entrambe le classi. Eseguendo nuovamente §4.3 con la correzione applicata
si ottiene `RDNs=4` per il principal craftato.
### 6.3 Test aggiunti
**File:** `core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java`
(+124 righe, 0 eliminazioni). Poiché `JndiLdapRealmTest extends DefaultLdapRealmTest`, ogni
test seguente viene eseguito su **entrambi** i realm — 10 esecuzioni da 5 metodi.
| Test | Verifica | Baseline |
|---|---|---|
| `testGetUserDnLeavesOrdinaryPrincipalUnchanged` | Un principal ordinario produce l'esatto DN atteso, 4 RDN, valore intatto. Protegge da over-escaping. | **passed** (guard) |
| `testGetUserDnPreservesTemplateStructure` | Per `jsmith,ou=admins` il DN ha ancora 4 componenti e il principal sopravvive come un unico valore di attributo. | failed |
| `testGetUserDnPreservesTemplateStructureForReservedCharacters` | Lo stesso, su tutti i 10 principal con caratteri riservati; fallisce il test anche se il DN non è un nome ben formato. | failed |
| `testGetUserDnEncodesSubstitutedValue` | Il valore sostituito è codificato in modo tale che il round-trip restituisca il principal inviato. | failed |
| `testUserDnTemplateSubstitutionPreservesStructure` | End-to-end attraverso `getAuthenticationInfo`: il DN passato a `LdapContextFactory` mantiene la struttura del template. | failed |
**Nota di progettazione.** Le asserzioni strutturali analizzano il risultato con `javax.naming.ldap.LdapName`
e confrontano il numero di componenti e il valore foglia sottoposto a round-trip, invece di asserire una
stringa escaped attesa. I test verificano quindi l'effettiva proprietà richiesta e non
presuppongono `Rdn.escapeValue` come implementazione — un encoder corretto alternativo
passerebbe comunque.
Comando:```bash
mvn -B clean verify
La build completa del reactor passa con nessun flag e nessuno skip:``` mvn -B clean verify
**BUILD SUCCESS — 907 test, 0 fallimenti, 0 errori, 3 saltati, sull'intero reactor.**
Ogni gate è attivo in questa esecuzione: unit test (surefire), integration test (failsafe),
l'audit delle licenze Apache RAT, maven-enforcer e japicmp.
| Gate | Risultato |
|---|---|
| Unit test, intero reactor | **907 eseguiti, 0 fallimenti, 0 errori, 3 saltati** |
| Solo modulo `core` | **321 eseguiti, 0 fallimenti, 0 errori, 0 saltati** |
| Test LDAP (`DefaultLdapRealmTest` + `JndiLdapRealmTest`) | **32 eseguiti, 0 fallimenti** |
| Integration test (failsafe) | eseguiti sull'intero reactor, nessun fallimento |
| Audit delle licenze Apache RAT | Non approvate: 0, sconosciute: 0 |
| maven-enforcer | nessuna violazione |
| japicmp | nessuna incompatibilità segnalata |
I 3 test saltati sono `@Ignore` preesistenti in moduli non correlati a questa modifica; `core` — l'unico
modulo toccato — non salta nulla.
## Riferimenti
- Record CVE (MITRE API): `https://cveawg.mitre.org/api/cve/CVE-2026-49268`
- Annuncio upstream: `https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s`
- Segnalazioni di sicurezza di Apache Shiro: `https://shiro.apache.org/security-reports.html`
- RFC 2253 / RFC 4514 — Rappresentazione stringa LDAP dei Distinguished Names
- RFC 4515 — Rappresentazione stringa dei filtri di ricerca LDAP
## Contatti
Autore della ricerca e della remediation della vulnerabilità di sicurezza: Jinwoo Hwang ([https://JinwooHwang.com](https://jinwoohwang.com/))
| Campo | Valore |
|---|
| CVE | CVE-2026-49268 — Apache Shiro: LDAP DN Injection in DefaultLdapRealm |
| CWE ID | CWE-90 — Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection') |
| CVSS v4.0 | 8.8 HIGH — 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 CRITICAL — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Versioni interessate | org.apache.shiro:shiro-core dalla 0 alla 2.2.0 inclusa; dalla 3.0.0-alpha-0 alla 3.0.0-alpha-1 inclusa |
| Corretto upstream in | 2.2.1 e 3.0.0-alpha-2 |
| Riferimento | https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s |
| Principal | Risultato baseline | Effetto |
|---|
jsmith,ou=admins | analizzato, 5 RDN | struttura alterata — viene interposto un contenitore |
jsmith;ou=admins | analizzato, 5 RDN | struttura alterata — ; è un separatore RDN secondo RFC 1779 |
jsmith+uid=admin | analizzato, 4 RDN | diventa un RDN multi-valore; uid acquisisce un secondo valore |
jsmith=admin | analizzato, 4 RDN | valore corrotto |
quo"te, angle<br>ackets, leadingSpace, trailingSpace | analizzati, 4 RDN | valore corrotto |
back\slash | IllegalArgumentException | malformato — il bind non può essere tentato |
#leadingNumberSign | IllegalArgumentException | malformato — il bind non può essere tentato |
| Livello | Componente |
|---|
| Client HTTP | HttpURLConnection, POST form grezzo |
| Servlet container | Jetty 9.4.58.v20250814 embedded |
| Filtro di sicurezza | ShiroFilter + EnvironmentLoaderListener, authc (FormAuthenticationFilter) |
| Realm | DefaultLdapRealm, userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com |
| Directory | UnboundID InMemoryDirectoryServer, strumentato per registrare ogni bind DN |
| Richiesta | Affetto | Corretto |
|---|
username=jsmith&password=userpass | 302 autenticato | 302 autenticato |
username=jsmith%2Cou%3Dadmins&password=adminpass | 302 autenticato | 200 rifiutato |
username=jsmith%2Cou%3Dadmins&password=userpass | 200 rifiutato | 200 rifiutato |