02 — Root Cause Analysis: CWE-91
CWE-91: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
While CWE-91 is generically about "Special Elements," in the context of Log4j it refers to the improper handling of JNDI lookup syntax in log messages.
Technical Breakdown
How Log4j Processes Messages
Log4j has a feature called "Lookups" that allows dynamic value insertion in log messages:
// Standard logging
logger.info("User: " + username); // Output: User: alice
// With Log4j Lookups
logger.info("User: ${java:version}"); // Output: User: 11.0.13
// JNDI Lookup (the vulnerable feature)
logger.info("Session: ${jndi:ldap://ldap.example.com/ou=Sessions}");
The problem: Log4j trusts the input without validating what comes after jndi:.
The Vulnerability Chain
User Input (untrusted)
↓
Log4j receives input in log message
↓
Log4j parses $ {...} pattern
↓
Creates JNDI Context
↓
Attempts to load remote resource (LDAP, RMI, DNS)
↓
Remote server responds with malicious serialized object
↓
Java deserializes object
↓
Code in object constructor executes
↓
✓ Remote Code Execution (RCE)
Why This is CWE-91
CWE-91 definition: "Improper Neutralization of Special Elements used in an SQL Command"
In Log4j context (adapted): Improper Neutralization of Special Elements used in a Lookup Expression
- Special Element: The
${jndi:...}syntax - Improper Neutralization: Log4j treats it as trusted code instead of untrusted data
- Attack Surface: Any log message that might contain user input
Code-Level Analysis
Vulnerable Code (Simplified - Log4j 2.14.x):
public class StrSubstitutor {
public String replace(String source) {
if (source == null) {
return null;
}
// Parse for ${...} patterns
// If pattern is ${jndi:*}, create JNDI lookup
// Execute the lookup WITHOUT validation
return resolveVariable(source); // ← VULNERABILITY HERE
}
private String resolveVariable(String value) {
if (value.startsWith("jndi:")) {
// No validation - just execute!
return executeJndi(value); // ← UNVALIDATED EXECUTION
}
return value;
}
}
Patched Code (Log4j 2.17.0+):
public class StrSubstitutor {
private static final boolean IGNORE_LOOKUPS =
!Boolean.parseBoolean(System.getProperty("log4j2.enableJndiLookup", "false"));
public String replace(String source) {
if (source == null) {
return null;
}
// JNDI lookups disabled by default in patched version
if (IGNORE_LOOKUPS && source.contains("jndi:")) {
return source; // Don't process JNDI lookups
}
return resolveVariable(source);
}
}
The fix: Disable JNDI lookups by default and require explicit opt-in.
Related Weaknesses
CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
- Similar: Untrusted code/expressions executed by runtime
- Different: Execution context (eval vs. resource lookup)
CWE-502: Deserialization of Untrusted Data
- Similar: Remote object deserialization
- Related: JNDI returns serialized Java objects that are deserialized
CWE-94: Improper Control of Generation of Code ('Code Injection')
- Similar: Dynamic code execution
- Different: Direct code generation vs. lookup expression
Attack Surface in Log4j
Any part of your application that logs user-controlled data is attack surface:
| Source | Example | KGCS Category |
|---|---|---|
| HTTP query parameters | ?search=${jndi:ldap://...} |
User Input |
| HTTP headers | User-Agent: ${jndi:ldap://...} |
User Input |
| Form fields | Username field contains payload | User Input |
| API JSON payloads | {"email": "${jndi:ldap://...}"} |
User Input |
| Reverse DNS logs | Server logs reverse DNS, which contains payload | Context-Dependent |
| Email headers | Email subject/sender | Context-Dependent |
| Browser user agent | Logged for analytics | Consumer Input |
Exploitation Prerequisites
An attacker needs:
- ✓ A way to inject at least one log message containing
${jndi:...} - ✓ That message must be processed by vulnerable Log4j
- ✓ Access to external resource server (LDAP, RMI, or DNS)
- ✓ Network connectivity from target to attacker's infrastructure
All four conditions are almost always met in real deployments.
Why This Weakness is Dangerous
Design Issue
The original Lookup feature was designed with assumption: "Log messages are internal, trusted, generated by developers."
This assumption was violated in modern applications where:
- Logs include user input
- Logs are accessible/parseable
- Applications trust libraries too much
Defense-in-Depth Failure
Standard defenses don't prevent this:
- WAF Rules: Can't catch all
${jndi:payloads (too much variation) - Input Validation: Apps validate for app logic, not for Log4j quirks
- Intrusion Detection: Signature-based detection of specific payloads only
Mitigation at the Code Level
For Application Developers
// BAD: Logs user input directly
String userInput = request.getParameter("q");
logger.info("Search: " + userInput); // VULNERABLE if userInput contains ${jndi:...}
// BETTER: Sanitize/escape user input
String userInput = request.getParameter("q");
String sanitized = userInput.replace("$", ""); // Remove $ chars
logger.info("Search: " + sanitized); // Safer but not foolproof
// BEST: Upgrade Log4j and disable lookups
// (System admin responsibility, not application code)
For Log4j Developers
The fix in 2.17.0:
- Disable JNDI lookups by default (breaking change, but necessary)
- Require
log4j2.enableJndiLookup=trueJVM flag to enable them - Document the security implications
KGCS Mapping
In the KGCS graph:
CVE-2021-44228 (specific instance of the weakness)
↓ CAUSED_BY
CWE-91 (general weakness class)
↓ DEMONSTRATED_BY (how to exploit it)
[CAPEC-88: OS Command Injection]
[CAPEC-242: Code Injection via Lookup Expression]
All three are connected in KGCS, showing that:
- One CWE can have multiple CVEs
- One CVE can result from multiple attack patterns
- Fixing CWE-91 prevents all variants
Next Steps
Read: 03 Attack Scenarios — See how attackers weaponize this weakness
Or Jump To: 06 Reasoning Walkthrough — See KGCS analysis in action
Sources: CWE-91 Definition, CVE-2021-44228 Advisory, Log4j Security Docs, Apache Security Guide