CWE-94: Code Injection - Java
Overview
Code Injection in Java occurs when untrusted input is evaluated as executable code at runtime, rather than treated as data. Java applications are vulnerable when they use scripting engines (javax.script.ScriptEngine with JavaScript, Groovy, or MVEL), pass user input to Groovy's GroovyShell.evaluate(), or configure template engines without sandboxing. The ScriptEngine API is particularly dangerous because it can be exposed unintentionally through configuration-driven features.
Unlike OS command injection (CWE-78), code injection allows the attacker to execute arbitrary logic within the running JVM process itself - with full access to the classpath, file system, and network. This makes it one of the most severe vulnerability classes.
Primary Defence: Replace dynamic code evaluation with static logic, lookup tables, or a restricted expression evaluator (e.g., Spring Expression Language scoped to read-only property access, not method invocation).
Common Vulnerable Patterns
ScriptEngine with User Input
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import org.springframework.web.bind.annotation.*;
@RestController
public class CalculatorController {
private final ScriptEngine engine =
new ScriptEngineManager().getEngineByName("JavaScript");
@GetMapping("/calculate")
public String calculate(@RequestParam String expression) throws Exception {
// VULNERABLE - evaluates arbitrary JavaScript including java.lang.Runtime.exec()
Object result = engine.eval(expression);
return result.toString();
}
}
Why this is vulnerable:
ScriptEngine.eval()executes the string as JavaScript. An attacker can passjava.lang.Runtime.getRuntime().exec('id')or load arbitrary Java classes via reflection.- There is no safe way to sandbox Nashorn/Rhino when user input can reach
eval().
Groovy Dynamic Evaluation
import groovy.lang.GroovyShell;
public class RuleEngine {
public Object evaluate(String userRule) {
// VULNERABLE - GroovyShell gives full JVM access to the script
GroovyShell shell = new GroovyShell();
return shell.evaluate(userRule);
}
}
Why this is vulnerable:
- Groovy scripts have unrestricted access to the JVM. An attacker can read files, open network connections, or execute OS commands from within the script.
GroovyClassLoader.parseClass()andGroovyScriptEngineare equally dangerous.
SpEL with Unrestricted Method Invocation
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@RestController
public class TemplateController {
private final ExpressionParser parser = new SpelExpressionParser();
@PostMapping("/render")
public String render(@RequestBody String template) {
// VULNERABLE - SpEL can call T(Runtime).exec() with user-supplied input
return parser.parseExpression(template).getValue(String.class);
}
}
Why this is vulnerable:
- SpEL supports the
T()operator that references arbitrary Java classes:T(java.lang.Runtime).getRuntime().exec('id')is valid SpEL syntax.
Secure Patterns
Lookup Table (Recommended for Configurable Operations)
import java.util.Map;
import java.util.function.Function;
import org.springframework.web.bind.annotation.*;
@RestController
public class CalculatorController {
// SECURE: predefined operations - no dynamic evaluation
private static final Map<String, Function<Double, Double>> OPERATIONS = Map.of(
"double", x -> x * 2,
"square", x -> x * x,
"negate", x -> -x,
"sqrt", Math::sqrt
);
@GetMapping("/calculate")
public double calculate(@RequestParam String operation, @RequestParam double value) {
Function<Double, Double> op = OPERATIONS.get(operation);
if (op == null) {
throw new IllegalArgumentException("Unknown operation: " + operation);
}
return op.apply(value);
}
}
Why this works:
- The lookup table maps string identifiers to predefined Java lambdas. User input selects from this fixed set - it can never introduce new logic.
- If the key is not in the map, the request is rejected before any computation occurs.
Strategy Pattern for Pluggable Business Rules
public interface PricingRule {
double apply(double basePrice, int quantity);
}
@Component("bulk")
class BulkDiscount implements PricingRule {
public double apply(double basePrice, int quantity) {
return quantity > 10 ? basePrice * 0.9 : basePrice;
}
}
@Service
public class PricingService {
private final Map<String, PricingRule> rules; // injected by Spring by bean name
public PricingService(Map<String, PricingRule> rules) {
this.rules = rules;
}
// SECURE: user selects a named rule; the logic is always predefined Java code
public double applyRule(String ruleName, double basePrice, int quantity) {
PricingRule rule = rules.get(ruleName);
if (rule == null) {
throw new IllegalArgumentException("Unknown rule: " + ruleName);
}
return rule.apply(basePrice, quantity);
}
}
Why this works:
- The strategy pattern delegates to concrete Java classes defined at compile time. User input only selects a name - it never supplies logic.
Apache Commons JEXL with Sandboxing (When Scripting is Genuinely Required)
import org.apache.commons.jexl3.*;
import java.util.Map;
// SECURE: JEXL with a JexlSandbox that blocks access to system classes
public class SafeExpressionEvaluator {
private final JexlEngine engine;
public SafeExpressionEvaluator() {
JexlSandbox sandbox = new JexlSandbox(false); // deny all by default
sandbox.allow(Double.class.getName());
sandbox.allow(Math.class.getName());
// System, Runtime, File, etc. are all blocked
this.engine = new JexlBuilder()
.sandbox(sandbox)
.strict(true)
.create();
}
public Object evaluate(String expression, Map<String, Object> variables) {
if (expression == null || expression.length() > 200) {
throw new IllegalArgumentException("Expression too long or null");
}
JexlContext context = new MapContext(variables);
return engine.createExpression(expression).evaluate(context);
}
}
Why this works:
JexlSandbox(false)denies access to all classes by default. Only explicitly permitted classes can be referenced, blockingSystem.exit(),Runtime.exec(), file I/O, and reflection.
SpEL with SimpleEvaluationContext
import org.springframework.expression.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
// SECURE: SimpleEvaluationContext restricts SpEL to property access only
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// T() operator and method invocation are blocked in SimpleEvaluationContext
String result = parser.parseExpression(template).getValue(context, myBean, String.class);
Why this works:
SimpleEvaluationContextdisables theT()type reference operator and method invocation, making it impossible to referenceRuntime,System, or other dangerous classes.
Remediation Steps
Locate the Finding
- Source: User-controlled input -
request.getParameter(),@RequestParam,@RequestBody, config values - Sink:
ScriptEngine.eval(),GroovyShell.evaluate(),GroovyClassLoader.parseClass(),SpelExpressionParser.parseExpression()withoutSimpleEvaluationContext
Apply the Fix
- PRIORITY 1: Remove dynamic evaluation. Replace with a
Map<String, Callable>or strategy pattern. - PRIORITY 2: For genuine scripting requirements, use Apache Commons JEXL with
JexlSandbox(false)and an explicit allowlist. - PRIORITY 3: For SpEL, switch to
SimpleEvaluationContextwhich disables class references and method invocation.
Verify the Fix
- Submit
T(java.lang.Runtime).getRuntime().exec('id')as an expression and confirm an exception is thrown - Submit
java.lang.System.exit(1)and confirm the JVM does not exit - Rescan with the security scanner to confirm the finding is resolved
Check for Similar Issues
Search for: ScriptEngine, GroovyShell, GroovyClassLoader, parseExpression, BeanShell, MVEL
Testing
- Normal input: exercise each supported named operation, strategy, or restricted expression the application still needs.
- Boundary input: test unknown operation names, long expressions, nested properties, and invalid syntax.
- Malicious input: submit
T(java.lang.Runtime).getRuntime().exec('id'),System.exit(1), file access, reflection, and class-loading payloads; confirm they are rejected before execution.