
CVE-2026-49268 — Analysis and Remediation of an LDAP Injection Authentication Bypass Vulnerability
| Branch | 1.13-CVE-2026-49268 |
| Author | Jinwoo Hwang (https://JinwooHwang.com) |
This branch (1.13-CVE-2026-49268) contains a comprehensive security remediation of an LDAP Injection Authentication Bypass Vulnerability in Apache Shiro 1.13 release.
Official NVD Description
A remote attacker can inject LDAP special characters into the Distinguished Name (DN) construction in DefaultLdapRealm class. User-supplied username input is directly concatenated into the LDAP DN template without any escaping of RFC 2253 special characters. This allows an attacker to manipulate the DN structure used for LDAP bind authentication, potentially bypassing authentication or impersonating other users. This issue affects all Apache Shiro versions through 2.2.0, and 3.0.0-alpha-1 when using DefaultLdapRealm.
| Field | Value |
|---|---|
| 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 |
| Affected versions | org.apache.shiro:shiro-core 0 through 2.2.0 inclusive; 3.0.0-alpha-0 through 3.0.0-alpha-1 inclusive |
| Fixed upstream in | 2.2.1 and 3.0.0-alpha-2 |
| Reference | https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s |
Apache Shiro can authenticate users against an LDAP directory. To do that, it turns a
submitted username into a Distinguished Name — the directory's address for that user's
entry — by dropping the username into a configured template, for example
uid={0},ou=users,dc=mycompany,dc=com.
In the affected versions the username is pasted into that template as raw text. A handful of punctuation characters — most importantly the comma — are not ordinary text to a directory server; they are the syntax that separates one part of an address from the next. A username containing those characters therefore stops behaving like a value inside the address and starts behaving like part of the address itself.
The practical consequence is that a person logging in can influence which directory entry Shiro tries to authenticate against, rather than only supplying their own name. Instead of being looked up in the container the deployment intended, the lookup can be redirected elsewhere in the directory. Depending on how the directory is laid out and what it permits, this can lead to authenticating as the wrong identity, or to authentication succeeding when it should not.
Risk profile. What raises the risk: no prior access or credentials are required — the
input arrives at the login boundary, which is reachable by anyone who can reach the
application; and the affected class is the standard, documented way to wire Shiro to LDAP,
so this is not an exotic configuration. What lowers it: the deployment must actually use
DefaultLdapRealm (or JndiLdapRealm) with a configured DN template; deployments that
authenticate by other means, or that pass a full DN or a non-text credential such as a
certificate, are not affected. Whether a redirected lookup yields a usable authentication
also depends on the target directory's own layout and access rules, which vary by site.
A second, non-security consequence is worth noting for release planning: users whose
legitimate usernames contain a backslash or begin with # currently cannot log in at
all, because the address built for them is not a well-formed name and is rejected
outright. Other punctuation does not fail outright — it silently changes which entry the
address refers to, which is the security problem above. The same fix resolves both.
core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java, getUserDn(String),
lines 227–250 on the unfixed baseline:
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();
}
The template is split once, at configuration time, around the {0} token
(setUserDnTemplate, lines 181–200), into a prefix and a suffix. At authentication time
the principal is concatenated between them. No encoding is applied at any point. There is
no escaping helper anywhere in org/apache/shiro/realm/ldap/.
The surrounding code treats the result of getUserDn as a well-formed Distinguished Name —
it is handed straight to LdapContextFactory.getLdapContext(...) and from there to JNDI as a
bind DN. That is only sound if the substituted principal is a single attribute value.
String concatenation cannot enforce that. RFC 2253 (and RFC 4514) reserve
, + " \ < > ; =, a leading #, and leading/trailing whitespace as structural
syntax within a DN. When any of those appear in the principal they are read by the parser as
structure, not as content. The invariant — "the principal occupies exactly one RDN value" —
was assumed by every downstream consumer and enforced by none.
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: