Remote Code Execution via ScriptEngine Injection

Critical Risk rce
javarcescript-injectionscriptenginecode-execution

What it is

Remote code execution vulnerabilities occur when untrusted user input is passed to ScriptEngine.eval(), allowing attackers to execute arbitrary code by injecting malicious scripts into the evaluation context.

@RestControllerpublic class ScriptController {        private ScriptEngine scriptEngine;        public ScriptController() {        ScriptEngineManager manager = new ScriptEngineManager();        // VULNERABLE: Using JavaScript engine with full access        this.scriptEngine = manager.getEngineByName("JavaScript");    }        @PostMapping("/api/calculate")    public ResponseEntity<Object> calculate(@RequestBody Map<String, String> request) {        String expression = request.get("expression");                try {            // VULNERABLE: Direct evaluation of user input            Object result = scriptEngine.eval(expression);            return ResponseEntity.ok(result);        } catch (ScriptException e) {            return ResponseEntity.badRequest().body("Invalid expression");        }    }        @PostMapping("/api/transform")    public ResponseEntity<String> transformData(@RequestBody Map<String, Object> request) {        String script = (String) request.get("script");        Object data = request.get("data");                try {            // VULNERABLE: User-controlled script execution            scriptEngine.put("inputData", data);            Object result = scriptEngine.eval(script);            return ResponseEntity.ok(result.toString());        } catch (ScriptException e) {            return ResponseEntity.status(500).body("Script execution failed");        }    }        @GetMapping("/api/dynamic-logic")    public ResponseEntity<Object> executeDynamicLogic(@RequestParam String logic) {        ScriptEngineManager manager = new ScriptEngineManager();        ScriptEngine engine = manager.getEngineByName("Groovy");                try {            // VULNERABLE: Creating new engine instance for each request            Object result = engine.eval(logic);            return ResponseEntity.ok(result);        } catch (ScriptException e) {            return ResponseEntity.badRequest().body("Logic execution error");        }    }}
@RestControllerpublic class SecureCalculatorController {        // SECURE: Use expression evaluator instead of script engine    private static final Set<String> ALLOWED_OPERATIONS = Set.of("+", "-", "*", "/", "(", ")");    private static final Pattern SAFE_EXPRESSION = Pattern.compile("^[0-9+\-*/().\s]+$");        @PostMapping("/api/calculate")    public ResponseEntity<Object> calculate(@RequestBody Map<String, String> request) {        String expression = request.get("expression");                // SECURE: Validate input before processing        if (!isValidMathExpression(expression)) {            return ResponseEntity.badRequest().body("Invalid mathematical expression");        }                try {            // SECURE: Use safe mathematical parser instead of script engine            double result = evaluateMathExpression(expression);            return ResponseEntity.ok(result);        } catch (Exception e) {            return ResponseEntity.badRequest().body("Calculation error");        }    }        private boolean isValidMathExpression(String expression) {        if (expression == null || expression.trim().isEmpty()) {            return false;        }                // Only allow mathematical characters        if (!SAFE_EXPRESSION.matcher(expression).matches()) {            return false;        }                // Additional validation for balanced parentheses, etc.        return isBalancedParentheses(expression);    }        private boolean isBalancedParentheses(String expression) {        int count = 0;        for (char c : expression.toCharArray()) {            if (c == '(') count++;            else if (c == ')') count--;            if (count < 0) return false;        }        return count == 0;    }        private double evaluateMathExpression(String expression) {        // SECURE: Use a proper mathematical expression parser        // This is a simplified example - use libraries like exp4j in production        return new MathExpressionParser().parse(expression).evaluate();    }        // SECURE: Using predefined templates instead of arbitrary scripts    @PostMapping("/api/transform")    public ResponseEntity<String> transformData(@RequestBody Map<String, Object> request) {        String templateName = (String) request.get("template");        Object data = request.get("data");                // SECURE: Allowlist of predefined transformation templates        Map<String, Function<Object, String>> templates = Map.of(            "uppercase", obj -> obj.toString().toUpperCase(),            "lowercase", obj -> obj.toString().toLowerCase(),            "reverse", obj -> new StringBuilder(obj.toString()).reverse().toString(),            "length", obj -> String.valueOf(obj.toString().length())        );                Function<Object, String> transformer = templates.get(templateName);        if (transformer == null) {            return ResponseEntity.badRequest().body("Invalid template name");        }                try {            String result = transformer.apply(data);            return ResponseEntity.ok(result);        } catch (Exception e) {            return ResponseEntity.status(500).body("Transformation failed");        }    }        // SECURE: Using configuration-based logic instead of dynamic scripts    @GetMapping("/api/business-logic")    public ResponseEntity<Object> executeBusinessLogic(@RequestParam String ruleId) {        // SECURE: Predefined business rules with IDs        Map<String, Supplier<Object>> businessRules = Map.of(            "discount-calculation", () -> calculateDiscount(),            "tax-calculation", () -> calculateTax(),            "shipping-cost", () -> calculateShipping()        );                Supplier<Object> rule = businessRules.get(ruleId);        if (rule == null) {            return ResponseEntity.badRequest().body("Invalid rule ID");        }                try {            Object result = rule.get();            return ResponseEntity.ok(result);        } catch (Exception e) {            return ResponseEntity.status(500).body("Business logic execution failed");        }    }        private Object calculateDiscount() {        // Predefined discount calculation logic        return Map.of("discount", 0.1, "type", "percentage");    }        private Object calculateTax() {        // Predefined tax calculation logic        return Map.of("tax_rate", 0.08, "type", "sales_tax");    }        private Object calculateShipping() {        // Predefined shipping calculation logic        return Map.of("cost", 5.99, "method", "standard");    }}// If scripting is absolutely necessary, use strict sandboxing@Componentpublic class SandboxedScriptExecutor {        private static final Set<String> ALLOWED_CLASSES = Set.of(        "java.lang.String",        "java.lang.Math",        "java.util.Collections"    );        public Object executeInSandbox(String script) throws ScriptException {        ScriptEngineManager manager = new ScriptEngineManager();        ScriptEngine engine = manager.getEngineByName("JavaScript");                // SECURE: Install security manager and restrictions        SecurityManager originalManager = System.getSecurityManager();        System.setSecurityManager(new RestrictiveSecurityManager());                try {            // Set limited execution context            engine.put("allowedClasses", ALLOWED_CLASSES);                        // Validate script doesn't contain dangerous operations            if (!isScriptSafe(script)) {                throw new SecurityException("Script contains dangerous operations");            }                        return engine.eval(script);        } finally {            System.setSecurityManager(originalManager);        }    }        private boolean isScriptSafe(String script) {        String[] dangerousKeywords = {            "java.lang.Runtime", "java.lang.Process", "java.io.File",            "java.net.Socket", "System.exit", "Class.forName",            "java.lang.reflect", "javax.script", "eval"        };                String lowerScript = script.toLowerCase();        for (String keyword : dangerousKeywords) {            if (lowerScript.contains(keyword.toLowerCase())) {                return false;            }        }                return true;    }        private static class RestrictiveSecurityManager extends SecurityManager {        @Override        public void checkPermission(Permission perm) {            if (perm instanceof java.io.FilePermission ||                perm instanceof java.net.SocketPermission ||                perm instanceof java.lang.RuntimePermission) {                throw new SecurityException("Operation not allowed in sandbox");            }        }    }}

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Passing user-provided data directly to ScriptEngine.eval() without sanitization, allowing code execution.

Root causes

User Input in ScriptEngine

Passing user-provided data directly to ScriptEngine.eval() without sanitization, allowing code execution.

Template Engine Misuse

Using JavaScript engines like Nashorn for user-facing templates instead of safe template libraries.

Plugin or Extension Systems

Building plugin systems that execute user-provided scripts without proper sandboxing.

Fixes

1

Avoid ScriptEngine with User Input

Never pass user input to ScriptEngine.eval(); use safe alternatives like expression languages with restricted features.

2

Use Safe Template Engines

Replace ScriptEngine-based templating with purpose-built template engines (Thymeleaf, Freemarker) with auto-escaping.

3

Implement Sandboxing

If script execution is required, use Java Security Manager or containerization to restrict script capabilities.

Detect This Vulnerability in Your Code

Sourcery automatically identifies remote code execution via scriptengine injection and many other security issues in your codebase.