
CVE-2026-49268 — एक LDAP इंजेक्शन प्रमाणीकरण बायपास भेद्यता का विश्लेषण और उपचार
| Branch | 1.13-CVE-2026-49268 |
| Author | Jinwoo Hwang (https://JinwooHwang.com) |
यह branch (1.13-CVE-2026-49268) Apache Shiro 1.13 release में एक LDAP Injection Authentication Bypass Vulnerability का व्यापक सुरक्षा उपचार समाहित करता है।
आधिकारिक NVD विवरण
एक remote attacker DefaultLdapRealm class में Distinguished Name (DN) निर्माण में LDAP विशेष वर्णों को inject कर सकता है। उपयोगकर्ता द्वारा प्रदान किया गया username input बिना किसी RFC 2253 विशेष वर्ण escaping के सीधे LDAP DN template में concatenate किया जाता है। यह attacker को LDAP bind authentication के लिए उपयोग की जाने वाली DN संरचना में हेरफेर करने की अनुमति देता है, जिससे संभवतः authentication bypass हो सकता है या अन्य उपयोगकर्ताओं का impersonation किया जा सकता है। यह समस्या DefaultLdapRealm का उपयोग करते समय सभी Apache Shiro संस्करणों को 2.2.0 तक, और 3.0.0-alpha-1 को प्रभावित करती है।
Apache Shiro उपयोगकर्ताओं को LDAP directory के विरुद्ध authenticate कर सकता है। ऐसा करने के लिए, यह जमा किए गए username को एक Distinguished Name में बदल देता है — उस उपयोगकर्ता की entry के लिए directory का पता — username को एक configured template में डालकर, उदाहरण के लिए
uid={0},ou=users,dc=mycompany,dc=com।
प्रभावित संस्करणों में username को उस template में raw text के रूप में paste किया जाता है। कुछ विराम चिह्न वर्ण — विशेष रूप से comma — directory server के लिए सामान्य text नहीं हैं; वे वह syntax हैं जो एक address के एक भाग को अगले से अलग करते हैं। इसलिए इन वर्णों वाला username address के भीतर एक value की तरह व्यवहार करना बंद कर देता है और स्वयं address का हिस्सा बनने लगता है।
व्यावहारिक परिणाम यह है कि login करने वाला व्यक्ति यह प्रभावित कर सकता है कि Shiro किस directory entry के विरुद्ध authenticate करने का प्रयास करता है, बजाय केवल अपना नाम प्रदान करने के। deployment जिस container में lookup चाहता था, उसमें खोजे जाने के बजाय lookup को directory में कहीं और redirect किया जा सकता है। directory कैसे व्यवस्थित है और क्या अनुमति देता है, इसके आधार पर यह गलत identity के रूप में authenticate होने, या authentication के सफल होने की ओर ले जा सकता है जबकि ऐसा नहीं होना चाहिए।
जोखिम प्रोफ़ाइल। जोखिम बढ़ाने वाले कारक: किसी पूर्व access या credentials की आवश्यकता नहीं है — input login boundary पर आता है, जो किसी भी व्यक्ति के लिए पहुँच योग्य है जो application तक पहुँच सकता है; और प्रभावित class Shiro को LDAP से जोड़ने का मानक, प्रलेखित तरीका है, इसलिए यह कोई असामान्य configuration नहीं है। जोखिम घटाने वाले कारक: deployment को वास्तव में DefaultLdapRealm (या JndiLdapRealm) एक configured DN template के साथ उपयोग करना चाहिए; जो deployments अन्य साधनों से authenticate करते हैं, या जो पूर्ण DN या प्रमाणपत्र जैसे non-text credential पास करते हैं, वे प्रभावित नहीं हैं। redirect किया गया lookup उपयोगी authentication देता है या नहीं, यह target directory के अपने layout और access rules पर भी निर्भर करता है, जो site के अनुसार भिन्न होते हैं।
एक दूसरा, गैर-सुरक्षा परिणाम release planning के लिए ध्यान देने योग्य है: जिन उपयोगकर्ताओं के वैध usernames में backslash है या जो # से शुरू होते हैं, वे वर्तमान में बिल्कुल भी login नहीं कर सकते, क्योंकि उनके लिए बनाया गया address एक well-formed name नहीं है और सीधे अस्वीकार कर दिया जाता है। अन्य विराम चिह्न सीधे विफल नहीं होते — वे चुपचाप बदल देते हैं कि address किस entry को संदर्भित करता है, जो ऊपर वर्णित सुरक्षा समस्या है। वही fix दोनों को हल करता है।
core/src/main/java/org/apache/shiro/realm/ldap/DefaultLdapRealm.java, getUserDn(String),
unfixed baseline पर पंक्तियाँ 227–250:```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();
}
टेम्पलेट को कॉन्फ़िगरेशन समय पर एक बार, `{0}` टोकन के आसपास (`setUserDnTemplate`, पंक्तियाँ 181–200), एक `prefix` और एक `suffix` में विभाजित किया जाता है। प्रमाणीकरण के समय principal को उनके बीच जोड़ा जाता है। **किसी भी बिंदु पर कोई एन्कोडिंग लागू नहीं की जाती है।** `org/apache/shiro/realm/ldap/` में कहीं भी कोई escaping helper नहीं है।
### 3.2 वह invariant जिसे मान लिया गया था पर कभी लागू नहीं किया गया
आसपास का कोड `getUserDn` के परिणाम को एक सुगठित Distinguished Name मानता है —
इसे सीधे `LdapContextFactory.getLdapContext(...)` को सौंपा जाता है और वहाँ से JNDI को
bind DN के रूप में। यह तभी उचित है जब प्रतिस्थापित principal एक *एकल attribute value* हो।
String concatenation इसे लागू नहीं कर सकता। RFC 2253 (और RFC 4514) DN के भीतर संरचनात्मक
सिंटैक्स के रूप में `,` `+` `"` `\` `<` `>` `;` `=`, एक अग्रणी `#`, और अग्रणी/पिछला whitespace
आरक्षित करते हैं। जब इनमें से कोई भी principal में प्रकट होता है तो उन्हें parser द्वारा
संरचना के रूप में पढ़ा जाता है, सामग्री के रूप में नहीं। invariant — *"principal ठीक एक RDN value
में स्थित होता है"* — को हर downstream consumer द्वारा मान लिया गया था और किसी के द्वारा लागू नहीं किया गया था।
### 3.3 निष्पादन प्रवाह, प्रवेश बिंदु से दोष तक```
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
टेम्पलेट uid={0},ou=users,dc=mycompany,dc=com, प्रिंसिपल jsmith,ou=admins:
| निर्मित DN | पार्स किए गए RDN | |
|---|---|---|
| इच्छित | uid=jsmith\,ou\=admins,ou=users,dc=mycompany,dc=com | 4 |
| बेसलाइन (अनफिक्स्ड) | uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com | 5 |
नाम में एक घटक जुड़ जाता है। ou=users अब संबोधित किया जाने वाला कंटेनर नहीं है —
ou=admins बीच में रखा गया है। javax.naming.ldap.LdapName से पार्स करके सत्यापित, जो
किसी भी एस्केपिंग कार्यान्वयन से स्वतंत्र है।
आरक्षित सेट में बेसलाइन का मापा गया व्यवहार (JDK 8, LdapName पार्स):
दो अक्षर नाम की संरचना को बदल देते हैं, न कि केवल उसकी सामग्री को: कॉमा, और
सेमीकोलन — जिसे RFC 1779 वैकल्पिक RDN विभाजक के रूप में स्वीकार करता है। + उसी RDN में
दूसरा एट्रिब्यूट मान जोड़ता है। केवल \ और अग्रणी # नाम को अपार्सनीय बनाते हैं।
userDnTemplate अनसेट। getUserDn प्रिंसिपल को अछूता लौटाता है। यह प्रलेखित
"प्रस्तुत प्रिंसिपल ही DN है" मोड है; कॉलर एक पूर्ण DN प्रदान करता है और उसकी
शुद्धता का स्वामी है। प्रभावित नहीं।String प्रिंसिपल। getLdapPrincipal केवल String प्रिंसिपल को
getUserDn में भेजता है; बाकी सब कुछ (X.509 प्रमाणपत्र, कस्टम टोकन) पास हो जाता है। प्रभावित नहीं।setUserDnTemplate सत्यापन (पंक्तियाँ 181–200) null, रिक्त, या
{0}-रहित टेम्पलेट को सही ढंग से अस्वीकार करता है। यह ऑपरेटर-आपूर्त टेम्पलेट को
सत्यापित करता है, जो कभी अविश्वसनीय इनपुट नहीं था — इसलिए यह सही कोड है जो बस
इस दोष को संबोधित नहीं करता।JndiLdapRealm DefaultLdapRealm का विस्तार करता है और getUserDn को ओवरराइड
नहीं करता, इसलिए यह दोष विरासत में लेता है। परीक्षण द्वारा पुष्टि (§6.1)। दायरे में, और
उसी परिवर्तन द्वारा ठीक किया गया।AbstractLdapRealm.searchFilter (पंक्ति 89) में एक दूसरा {0} प्रतिस्थापन है, डिफ़ॉल्ट
(&(objectClass=*)(userPrincipalName={0})), जो प्राधिकरण पथ पर उपयोग किया जाता है। यह प्रभावित नहीं है।
core, support और web मुख्य स्रोतों में प्रत्येक search( कॉल, प्रत्येक
getLdapContext( कॉलर, और प्रत्येक LDAP-आकार के स्ट्रिंग लिटरल की एक व्यापक जाँच में
searchFilter का ठीक एक उपयोग मिला — ActiveDirectoryRealm.getRoleNamesForUser, पंक्ति 172:```java
Object[] searchArguments = new Object[]{userPrincipalName};
NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
यह **parameterized** `DirContext.search(String, String, Object[], SearchControls)`
overload है। username को filter *argument* के रूप में पास किया जाता है, कभी भी filter
string में concatenate नहीं किया जाता। JDK 8 `javax.naming.directory.DirContext` javadoc के अनुसार, शब्दशः:
> "जब किसी variable के लिए string-valued filter argument प्रतिस्थापित किया जाता है, तो filter की
> व्याख्या इस प्रकार की जाती है जैसे कि string को variable के स्थान पर दिया गया हो, और filters के भीतर
> विशेष महत्व रखने वाले किसी भी character (जैसे `'*'`) को RFC 2254 के नियमों के
> अनुसार escape किया गया हो।"
JNDI provider escaping करता है। इसका ऐतिहासिक रिकॉर्ड field
declaration में ही मौजूद है।
**Source: `core/src/main/java/org/apache/shiro/realm/ldap/AbstractLdapRealm.java`, lines 88–89**```java
//SHIRO-115 - prevent potential code injection:
protected String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";
स्रोत: core/src/main/java/org/apache/shiro/realm/activedirectory/ActiveDirectoryRealm.java, पंक्तियाँ 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);
The username reaches the directory as `searchArguments[0]`, never as text spliced into
`searchFilter`. The `{0}` token in the filter is resolved by the JNDI provider, not by
Shiro.
`ActiveDirectoryRealm` line 108 passes the raw username to
`getLdapContext(username, password)`. That value becomes a single JNDI
`SECURITY_PRINCIPAL` environment entry; it is not substituted into a template and no DN is
constructed from it, so it is outside this defect's mechanism.
**Verified empirically against a live directory server.** The JNDI escaping was not taken
on trust: it was observed on the wire. An in-process LDAP server
(UnboundID `InMemoryDirectoryServer`) was instrumented with an
`InMemoryOperationInterceptor` that records the filter of every search request it receives,
and Shiro's default `searchFilter` was issued through the same parameterized JNDI call used
by `ActiveDirectoryRealm`, with a crafted argument:```
filter template : (&(objectClass=*)(userPrincipalName={0}))
argument passed : *)(uid=jsmith
server received : (&(objectClass=*)(userPrincipalName=\2a\29\28uid=jsmith))
*, ) और ( सर्वर पर \2a, \29 और \28 के रूप में पहुँचे — RFC 2254 के अनुसार escaped। फ़िल्टर ठीक दो clauses बनाए रखता है; argument तीसरा नहीं जोड़ सका।
नियंत्रण, वही template जिसमें argument को filter argument के रूप में पास करने के बजाय raw text के रूप में splice किया गया है:``` CONTROL, raw splice: (&(objectClass=)(userPrincipalName=)(uid=jsmith)) server received : (&(objectClass=)(userPrincipalName=)(uid=jsmith))
The control reaches the server with its structure altered — a third clause added and the
`userPrincipalName` test reduced to a wildcard. The parameterized form does not. This
confirms, by observation rather than by contract, that the `searchFilter` path is not
affected.
---
## 4. Step-by-Step Reproduction Steps
Deterministic, from a clean checkout. Working directory is the repository root throughout.
### 4.1 Prerequisites
| Requirement | Value used |
|---|---|
| JDK | **8** — the branch sets `jdk.version` 1.8. Results below were produced with Zulu 1.8.0_432 (arm64). Point `JAVA_HOME` at a JDK 8 installation before running any command in this section: |
| Build | Apache Maven, network access required (parent POM `org.apache:apache:38`) |
| Baseline | `origin/1.13.x` |
| Configuration | `userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com` |
| Directory server | **Not required for §4.3-§4.4** — the defect is in DN construction, before any network call, so those steps mock `LdapContextFactory`. §4.5 additionally reproduces the whole path against a **real** LDAP server over HTTP. |
Setting `JAVA_HOME` to a JDK 8 install:```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
mvn -v से पुष्टि करें, जो बताता है कि Maven किस JDK का उपयोग करेगा। नीचे दिए गए कमांड मानते हैं कि यह
पहले से सेट है।
git clone https://github.com/apache/shiro.git cd shiro git checkout -b repro origin/1.13.x
### 4.3 दोष को सीधे देखें
यह न्यूनतम ट्रिगर है, जो Shiro के बिल्ड से स्वतंत्र है। `Repro.java` को एक स्क्रैच
डायरेक्टरी में लिखें:```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());
}
}
इसे चलाएँ:```bash javac Repro.java && java Repro
प्रेक्षित आउटपुट:```
benign : uid=jsmith,ou=users,dc=mycompany,dc=com -> RDNs=4
crafted: uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com -> RDNs=5
कंपोनेंट की संख्या 4 से 5 हो जाती है। सबमिट किया गया मान name संरचना बन गया है।
unfixed baseline पर रिपॉज़िटरी रूट से, §6.3 में दिए गए परीक्षण लागू करें और चलाएँ:```bash mvn -B clean verify
The build `Apache Shiro :: Core` पर रुक जाता है और proving tests विफल हो जाते हैं — पूर्ण कैप्चर किया गया आउटपुट §6.1 में देखें:```
DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
पूरे स्टैक के माध्यम से पुनरुत्पादित — एक वास्तविक HTTP अनुरोध, एक वास्तविक सर्वलेट कंटेनर,
Shiro का स्वयं का FormAuthenticationFilter, और एक वास्तविक LDAP सर्वर। किसी भी
स्तर पर कुछ भी मॉक नहीं किया गया है।
डायरेक्टरी फ़िक्स्चर — एक नेस्टेड विशेषाधिकार प्राप्त कंटेनर, एक सामान्य वास्तविक-विश्व लेआउट:``` dc=mycompany,dc=com └── ou=users ├── uid=jsmith userPassword: userpass (ordinary account) └── ou=admins └── uid=jsmith userPassword: adminpass (privileged account)
#### 4.5.1 प्रभावित बिल्ड
**अनुरोध**```http
POST /login HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
username=jsmith%2Cou%3Dadmins&password=adminpass
प्रतिक्रिया```http HTTP/1.1 302 Found Location: http://127.0.0.1/;jsessionid=node0qu3v2n612vy71alsuyir2bgzz1.node0
**Bind DN जो directory को प्राप्त हुआ**```
uid=jsmith,ou=admins,ou=users,dc=mycompany,dc=com
302 FormAuthenticationFilter का सफल रीडायरेक्ट है: अनुरोध प्रमाणित हो गया।
DN में पाँच घटक हैं जबकि टेम्पलेट चार परिभाषित करता है, और पहुँची गई प्रविष्टि
ou=admins के अंतर्गत विशेषाधिकार प्राप्त है।
नियंत्रण — सामान्य खाते के पासवर्ड के साथ वही उपयोगकर्ता नाम```http POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded
username=jsmith%2Cou%3Dadmins&password=userpass
| `-s` | `--server` | Server URL (default: `http://localhost:8080`) |
| `-t` | `--token` | API token for authentication |
| `-o` | `--output` | Output file path |
| `-f` | `--format` | Output format: `json`, `yaml`, `table` |
| `-v` | `--verbose` | Enable verbose logging |
| `-q` | `--quiet` | Suppress non-essential output |
| `-h` | `--help` | Show help message |
| `-V` | `--version` | Show version information |
### उदाहरण
```bash
# Scan a single target
scanner scan --target example.com
# Scan multiple targets from a file
scanner scan --input targets.txt --output results.json
# Use a custom configuration file
scanner scan --config /path/to/config.yaml
# Enable verbose output
scanner scan --target example.com --verbose
The tool reads configuration from the following locations in order of precedence:
~/.scanner/config.yaml)# ~/.scanner/config.yaml
server:
url: "http://localhost:8080"
timeout: 30s
scan:
threads: 10
timeout: 60s
retries: 3
output:
format: "json"
verbose: false
The tool supports multiple output formats:
{
"scan_id": "abc123",
"target": "example.com",
"status": "completed",
"findings": [
{
"id": "FINDING-001",
"severity": "high",
"title": "Example vulnerability",
"description": "Detailed description of the finding"
}
]
}
scan_id: abc123
target: example.com
status: completed
findings:
- id: FINDING-001
severity: high
title: Example vulnerability
description: Detailed description of the finding
The tool returns the following exit codes:
Contributions are welcome! Please follow these guidelines:
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.
This tool is intended for authorized security testing only. Users are responsible for complying with all applicable laws and regulations. The authors assume no liability for misuse or damage caused by this tool.```http HTTP/1.1 200 OK
अस्वीकृत। यही वह बात है जो प्रतिरूपण स्थापित करती है, संयोग नहीं: तैयार किया गया
उपयोगकर्ता नाम **विशेषाधिकार प्राप्त खाते** के पासवर्ड से प्रमाणित होता है और साधारण
उपयोगकर्ता के पासवर्ड से विफल हो जाता है, इसलिए जिन क्रेडेंशियल्स की जाँच की जा रही है वे टेम्पलेट द्वारा संबोधित
की जा रही प्रविष्टि से भिन्न निर्देशिका प्रविष्टि के हैं।
#### 4.5.2 उपचारित बिल्ड — समान अनुरोध
| अनुरोध | प्रभावित | उपचारित |
|---|---|---|
| `username=jsmith&password=userpass` | `302` प्रमाणित | `302` प्रमाणित |
| `username=jsmith%2Cou%3Dadmins&password=adminpass` | **`302` प्रमाणित** | **`200` अस्वीकृत** |
| `username=jsmith%2Cou%3Dadmins&password=userpass` | `200` अस्वीकृत | `200` अस्वीकृत |
तैयार किए गए उपयोगकर्ता नाम के लिए निर्देशिका द्वारा प्राप्त Bind DN:```
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)
सामान्य लॉगिन अपरिवर्तित रहते हैं — पंक्ति 1 दोनों बिल्ड्स पर समान रूप से प्रमाणित होती है, जिसमें
डायरेक्टरी को uid=jsmith,ou=users,dc=mycompany,dc=com प्राप्त होता है।
दोनों रनों ने एक ही हार्नेस और एक ही फिक्स्चर का उपयोग किया; केवल क्लासपाथ पर shiro-core भिन्न था। वास्तव में कौन सी क्लास लोड हुई, इसकी पुष्टि प्रत्येक रन के लिए -verbose:class से की गई।
प्रतिस्थापन से पहले प्रिंसिपल को एकल DN एट्रिब्यूट वैल्यू के रूप में एन्कोड करें, JDK के
स्वयं के RFC 2253 एन्कोडर, javax.naming.ldap.Rdn.escapeValue का उपयोग करके। यह वही तंत्र है जिसका
उपयोग JDK नाम बनाने के लिए करता है, इसलिए इसका आउटपुट निर्माण के अनुसार उस पार्सर के साथ संगत होता है जो इसे उपभोग करता है।
फ़ाइल: `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);
}
**हंक क्या लागू करता है।** `Rdn.escapeValue` RFC 2253 आरक्षित सेट
(`\ , = + < > # ; "`) के साथ-साथ अग्रणी और अनुगामी व्हाइटस्पेस को बैकस्लैश-एस्केप करता है। इसके बाद, प्रतिस्थापित टेक्स्ट
केवल एक RDN मान पर ही कब्जा कर सकता है: पार्सर एस्केप किए गए अक्षरों को सामग्री के रूप में पढ़ता है, इसलिए
परिणाम की घटक संख्या टेम्पलेट द्वारा निर्धारित होती है और प्रिंसिपल द्वारा प्रभावित नहीं की जा सकती। `StringBuilder` क्षमता संकेत को एन्कोडेड लंबाई में अपडेट किया जाता है — यह एक आकार-निर्धारण विवरण है, व्यवहारिक नहीं।
**स्थान।** एन्कोडिंग अनकॉन्फ़िगर्ड-टेम्पलेट
मामले के लिए अर्ली रिटर्न के *बाद* आती है। यह जानबूझकर है: उस मोड में कॉलर एक पूर्ण DN प्रदान करता है और इसे एन्कोड करना
इसे दूषित कर देगा। दोनों शाखाएँ अपने मौजूदा अनुबंध बनाए रखती हैं।
### 5.2 वैध कॉलर्स के लिए व्यवहार परिवर्तन
यह फिक्स किसी भी आरक्षित
अक्षर वाले प्रिंसिपल के लिए उत्पन्न **DN स्ट्रिंग को बदल देता है**। तीन परिणाम स्पष्ट रूप से बताने योग्य हैं:
1. **पहले टूटे हुए लॉगिन अब काम करते हैं।** बैकस्लैश वाले या `#` से शुरू होने वाले
उपयोगकर्ता नामों ने एक ऐसा DN उत्पन्न किया जो एक सुगठित नाम नहीं था, इसलिए कोई बाइंड प्रयास ही नहीं किया जा सकता था। अब वे एक वैध DN में एन्कोड होते हैं और बाइंड का प्रयास करेंगे। साइटें ऐसे खातों को प्रमाणित होते हुए देख सकती हैं जो पहले नहीं हो सकते थे — यह एक फिक्स है, लेकिन एक दृश्यमान परिवर्तन है। `,`, `;`, `+` या `=` वाले उपयोगकर्ता नाम पहले अस्वीकार नहीं किए जाते थे; वे गलत
प्रविष्टि पर हल होते थे, और अब इच्छित प्रविष्टि पर हल होते हैं।
2. **निर्देशिका को भेजे गए बाइंड DN भिन्न होते हैं** प्रभावित उपयोगकर्ता नामों के लिए। निर्देशिका-पक्ष लॉग, ऑडिट ट्रेल्स, और कोई भी लॉग-स्क्रैपिंग जो शाब्दिक DN स्ट्रिंग्स पर मेल खाती है, एस्केप किए गए
रूपों (`uid=jsmith\,ou\=admins,...`) को देखेंगे। किसी कॉन्फ़िगरेशन परिवर्तन की आवश्यकता नहीं है।
3. **वे परिनियोजन जो उपयोगकर्ता नाम फ़ील्ड से बहु-घटक DN बनाने के लिए पुराने व्यवहार पर निर्भर थे, टूट जाएंगे।** यह एक समर्थित कॉन्फ़िगरेशन नहीं है — टेम्पलेट
संरचना को परिभाषित करने के लिए मौजूद है — लेकिन यह एकमात्र माइग्रेशन जोखिम है, और यह वही
व्यवहार है जिसे हटाने के लिए फिक्स मौजूद है।
`getUserDnTemplate()` को `getUserDn("{0}")` के रूप में लागू किया गया है। `{` और `}` RFC 2253 में आरक्षित नहीं हैं, इसलिए एक्सेसर का रिटर्न मान अपरिवर्तित है। पहले से मौजूद
`testUserDnTemplate` द्वारा पुष्टि की गई, जो बिना संशोधन के पास होता है।
---
## 6. सत्यापन और परीक्षण
### 6.1 पहले — बेसलाइन, अनफिक्स्ड
ब्रांच `1.13-CVE-2026-49268`, JDK Zulu 1.8.0_432। कमांड §4.4 के अनुसार।```
DefaultLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
JndiLdapRealmTest Tests run: 16, Failures: 4, Errors: 0, Skipped: 0
कैप्चर किया गया विफलता आउटपुट — यह साक्ष्य है, और एक बार समाधान लागू हो जाने के बाद यह पुनर्प्राप्त नहीं किया जा सकता:``` 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` **बेसलाइन पर पास हुआ**, जैसा कि इरादा था — यह
अति-सुधार के विरुद्ध एक गार्ड है, कोई प्रमाणित परीक्षण नहीं।
### 6.2 बाद में — फिक्स लागू होने के साथ
वही कमांड, वही JDK:```
DefaultLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
JndiLdapRealmTest Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
सभी चार प्रमाणन परीक्षण अब दोनों क्लासों पर पास होते हैं। फिक्स लागू होने के बाद §4.3 को दोबारा चलाने पर
तैयार किए गए प्रिंसिपल के लिए RDNs=4 प्राप्त होता है।
फ़ाइल: core/src/test/java/org/apache/shiro/realm/ldap/DefaultLdapRealmTest.java
(+124 पंक्तियाँ, 0 विलोपन)। चूँकि JndiLdapRealmTest extends DefaultLdapRealmTest, नीचे दिया गया प्रत्येक
परीक्षण दोनों रियल्म्स पर निष्पादित होता है — 5 मेथड्स से 10 निष्पादन।
डिज़ाइन नोट। संरचनात्मक दावे परिणाम को javax.naming.ldap.LdapName के साथ पार्स करते हैं
और अपेक्षित एस्केप्ड स्ट्रिंग का दावा करने के बजाय घटक गणनाओं और राउंड-ट्रिप किए गए लीफ मान की तुलना करते हैं।
इसलिए परीक्षण वास्तविक आवश्यक गुणधर्म की पुष्टि करते हैं और Rdn.escapeValue को कार्यान्वयन के रूप में
पूर्वनिर्धारित नहीं करते — एक वैकल्पिक सही एन्कोडर भी पास होगा।
कमांड:```bash mvn -B clean verify
### 6.4 रिग्रेशन — पूर्ण बिल्ड
पूर्ण रिएक्टर बिल्ड **बिना किसी फ्लैग और बिना किसी स्किप** के पास होता है:```
mvn -B clean verify
BUILD SUCCESS — 907 tests, 0 failures, 0 errors, 3 skipped, across the full reactor. इस रन में हर गेट सक्रिय है: यूनिट टेस्ट (surefire), इंटीग्रेशन टेस्ट (failsafe), Apache RAT लाइसेंस ऑडिट, maven-enforcer, और japicmp।
3 skips इस बदलाव से असंबंधित मॉड्यूल में पहले से मौजूद @Ignores हैं; core — जो
एकमात्र छुआ गया मॉड्यूल है — कुछ भी skip नहीं करता।
https://cveawg.mitre.org/api/cve/CVE-2026-49268https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5shttps://shiro.apache.org/security-reports.htmlसुरक्षा भेद्यता अनुसंधान और उपचार लेखक: Jinwoo Hwang (https://JinwooHwang.com)
| Field | Value |
|---|
| CVE | CVE-2026-49268 — Apache Shiro: DefaultLdapRealm में LDAP DN Injection |
| CWE ID | CWE-90 — 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 |
| प्रभावित संस्करण | org.apache.shiro:shiro-core 0 से 2.2.0 सम्मिलित; 3.0.0-alpha-0 से 3.0.0-alpha-1 सम्मिलित |
| Upstream में ठीक किया गया | 2.2.1 और 3.0.0-alpha-2 |
| संदर्भ | https://lists.apache.org/thread/svszql3od8td7hn6conyj2oq70v53b5s |
| प्रिंसिपल | बेसलाइन परिणाम | प्रभाव |
|---|
jsmith,ou=admins | पार्स होता है, 5 RDN | संरचना बदल गई — एक कंटेनर बीच में रखा गया |
jsmith;ou=admins | पार्स होता है, 5 RDN | संरचना बदल गई — RFC 1779 के अंतर्गत ; एक RDN विभाजक है |
jsmith+uid=admin | पार्स होता है, 4 RDN | एक बहु-मूल्यवान RDN बन जाता है; uid को दूसरा मान मिलता है |
jsmith=admin | पार्स होता है, 4 RDN | मान दूषित |
quo"te, angle<br>ackets, leadingSpace, trailingSpace | पार्स होते हैं, 4 RDN | मान दूषित |
back\slash | IllegalArgumentException | विकृत — बाइंड का प्रयास नहीं किया जा सकता |
#leadingNumberSign | IllegalArgumentException | विकृत — बाइंड का प्रयास नहीं किया जा सकता |
| परत | घटक |
|---|
| HTTP क्लाइंट | HttpURLConnection, रॉ फ़ॉर्म POST |
| सर्वलेट कंटेनर | एम्बेडेड Jetty 9.4.58.v20250814 |
| सुरक्षा फ़िल्टर | ShiroFilter + EnvironmentLoaderListener, authc (FormAuthenticationFilter) |
| रियल्म | DefaultLdapRealm, userDnTemplate = uid={0},ou=users,dc=mycompany,dc=com |
| डायरेक्टरी | UnboundID InMemoryDirectoryServer, प्रत्येक बाइंड DN को रिकॉर्ड करने के लिए इंस्ट्रुमेंटेड |
| Variable | Description | Default |
|---|
SCANNER_SERVER_URL | Server URL | http://localhost:8080 |
SCANNER_TOKEN | API token | (none) |
SCANNER_THREADS | Number of threads | 10 |
SCANNER_TIMEOUT | Request timeout | 60s |
SCANNER_LOG_LEVEL | Log level | info |
| Code | Description |
|---|
0 | Success |
1 | General error |
2 | Invalid arguments |
3 | Authentication failure |
4 | Network error |
5 | Configuration error |
| परीक्षण | दावा करता है | बेसलाइन |
|---|
testGetUserDnLeavesOrdinaryPrincipalUnchanged | एक साधारण प्रिंसिपल सटीक अपेक्षित DN, 4 RDNs, मान अक्षुण्ण देता है। अति-एस्केपिंग के विरुद्ध सुरक्षा करता है। | पास (गार्ड) |
testGetUserDnPreservesTemplateStructure | jsmith,ou=admins के लिए DN में अभी भी 4 घटक होते हैं और प्रिंसिपल एक एट्रिब्यूट मान के रूप में बचा रहता है। | विफल |
testGetUserDnPreservesTemplateStructureForReservedCharacters | वही, सभी 10 आरक्षित-वर्ण प्रिंसिपल्स पर; यदि DN एक सुगठित नाम नहीं है तो भी परीक्षण विफल होता है। | विफल |
testGetUserDnEncodesSubstitutedValue | प्रतिस्थापित मान इस प्रकार एन्कोड किया जाता है कि वह सबमिट किए गए प्रिंसिपल तक राउंड-ट्रिप करता है। | विफल |
testUserDnTemplateSubstitutionPreservesStructure | getAuthenticationInfo के माध्यम से एंड-टू-एंड: LdapContextFactory को दिया गया DN टेम्पलेट की संरचना बनाए रखता है। | विफल |
| गेट | परिणाम |
|---|
| यूनिट टेस्ट, पूरा reactor | 907 run, 0 failures, 0 errors, 3 skipped |
केवल core मॉड्यूल | 321 run, 0 failures, 0 errors, 0 skipped |
LDAP टेस्ट (DefaultLdapRealmTest + JndiLdapRealmTest) | 32 run, 0 failures |
| इंटीग्रेशन टेस्ट (failsafe) | पूरे reactor में चले, कोई विफलता नहीं |
| Apache RAT लाइसेंस ऑडिट | Unapproved: 0, unknown: 0 |
| maven-enforcer | कोई उल्लंघन नहीं |
| japicmp | कोई असंगति रिपोर्ट नहीं हुई |