Description
In Java and Kotlin, treating untrusted text as a SpEL expression, Janino script, or JSR-223 script source creates a code-injection risk.
SpEL can invoke constructors and methods and access properties, fields, and Spring beans. Janino compiles supplied Java statements into bytecode. JSR-223 executes source in the selected engine's language or compiles it for reuse. ScriptEngine.eval executes immediately; SpEL parsing, Janino compilation, and Compilable.compile require subsequent evaluation to execute the result. Depending on the engine and exposed context or bindings, attacker-controlled code may run with the application's privileges.
Spring Framework 7.0.9 warns that evaluating untrusted SpEL expressions is inherently dangerous and should generally be avoided. StandardEvaluationContext exposes the full language, while SimpleEvaluationContext provides only best-effort restrictions, not a safety guarantee.
The Java SE 25 ScriptEngine API distinguishes script source from values and objects supplied through Bindings or ScriptContext. Pass untrusted values as data to fixed scripts. Restricting the engine name or choosing a trusted implementation does not make untrusted script source safe.
Potential impact
- Arbitrary code or dangerous method execution with application privileges.
- Disclosure or alteration of files, databases, credentials, or other confidential data.
- Process execution, network access, or opportunities for privilege escalation.
- CPU or memory exhaustion through excessive compilation or evaluation.
Remediation
- Keep SpEL expressions, Janino scripts, and JSR-223 source as fixed, application-managed strings. Do not parse, compile, or evaluate untrusted text as code.
- For SpEL, use fixed expressions and pass request values only as variables, simple root-object data, or function arguments. Never evaluate untrusted expressions with
StandardEvaluationContext, and do not treatSimpleEvaluationContextas a sanitizer or safety guarantee. - For Janino, compile fixed scripts, declare inputs with
setParameters, and pass request values asevaluatearguments rather than concatenating them into source. - For JSR-223, evaluate fixed scripts and pass request values through
BindingsorScriptContext. Expose only necessary values and objects; engine selection is not source validation. - Map user-selected operations through a finite set of server-managed identifiers to reviewed fixed expressions or scripts. Length checks, character escaping, or a helper named
sanitizedo not by themselves make executable input safe. - If users must write executable code, isolate it in a separate process or container outside the application. Apply least privilege and limits on time, CPU, memory, filesystem access, and networking. The Security Manager cannot be enabled from JDK 24 onward; do not rely on it or Janino's legacy sandbox guidance.
Examples
SpEL
Before
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class DocumentController {
private final SpelExpressionParser parser = new SpelExpressionParser();
@GetMapping("/document")
String evaluate(@RequestParam String expressionText) {
Expression expression = parser.parseExpression(expressionText);
return expression.getValue(new StandardEvaluationContext(), String.class);
}
}
The request value is parsed as expression syntax and evaluated in a context exposing the full SpEL language.
After
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class DocumentController {
private static final Expression TEXT_EXPRESSION =
new SpelExpressionParser().parseExpression("#text");
@GetMapping("/document")
String render(@RequestParam String text) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("text", text);
return TEXT_EXPRESSION.getValue(context, String.class);
}
}
The application fixes the expression and passes the request value only as data in a variable.
Janino
Before
import org.codehaus.janino.ScriptEvaluator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class ScriptController {
@PostMapping("/script")
Object run(@RequestParam String script) throws Exception {
ScriptEvaluator evaluator = new ScriptEvaluator();
evaluator.cook(script);
return evaluator.evaluate();
}
}
The request value is compiled as a Java script body and immediately evaluated.
After
import org.codehaus.janino.ScriptEvaluator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class ScriptController {
@PostMapping("/normalize")
String normalize(@RequestParam String text) throws Exception {
ScriptEvaluator evaluator = new ScriptEvaluator();
evaluator.setReturnType(String.class);
evaluator.setParameters(
new String[] { "input" },
new Class<?>[] { String.class }
);
evaluator.cook("return input.trim();");
return (String) evaluator.evaluate(new Object[] { text });
}
}
The compiled script is fixed; the request value is supplied only as a runtime argument for the declared parameter.
JSR-223
Before
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class AutomationController {
private final ScriptEngine engine;
AutomationController(ScriptEngine engine) {
this.engine = engine;
}
@PostMapping("/automation")
Object run(@RequestParam String script) throws ScriptException {
return engine.eval(script);
}
}
The request value is immediately executed as JSR-223 source. Its language and accessible features depend on the injected engine and bindings.
After
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
class AutomationController {
private static final String FIXED_SCRIPT = "input";
private final ScriptEngine engine;
AutomationController(ScriptEngine engine) {
this.engine = engine;
}
@PostMapping("/automation")
Object run(@RequestParam String input) throws ScriptException {
Bindings bindings = engine.createBindings();
bindings.put("input", input);
return engine.eval(FIXED_SCRIPT, bindings);
}
}
The script is a server-managed constant appropriate for the chosen engine's language. The request value is passed only as data in Bindings.
Usage considerations
Distinguish parsing or compilation from actual evaluation or execution, and check subsequent use of the generated expression or code. Changing the input path or a helper's name does not remove the risk of treating untrusted text as executable source.
References
- Spring Framework 7.0.9 — SpEL Security Considerations
- Spring Framework — ExpressionParser API
- Janino 3.1.12 — ScriptEvaluator source and API
- Janino 3.1.12 — ICookable source and API
- Java SE 25 — ScriptEngine API
- Java SE 25 — Compilable API
- Java SE 25 — Bindings API
- OpenJDK JEP 486 — Permanently Disable the Security Manager
- OWASP Top 10:2025 A05 — Injection
- OWASP Top 10:2021 A03 — Injection
- CWE-94 — Improper Control of Generation of Code