Skip to content

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 pass java.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() and GroovyScriptEngine are 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

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, blocking System.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:

  • SimpleEvaluationContext disables the T() type reference operator and method invocation, making it impossible to reference Runtime, 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() without SimpleEvaluationContext

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 SimpleEvaluationContext which 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.

Additional Resources