Why is SpEL a security concern, and how do you evaluate potentially untrusted expressions safely?
answer
- T()/new/@ => full JVM access => RCE
- danger = untrusted expression TEXT, not data
- keep expression static; user data as #variables/root
- SimpleEvaluationContext disables T(), constructors, beans
- compilation != sandbox
basics
~20 sA full SpEL context can call any static method or constructor via T(), so evaluating attacker-controlled expressions enables remote code execution (e.g. T(Runtime).exec). Never build expressions from user input; if you must evaluate dynamic strings, use SimpleEvaluationContext, which disables T(), constructors, and bean access.
solid answer
~40 sSpEL is Turing-complete enough to be a code-execution vector: `StandardEvaluationContext` allows `T(java.lang.Runtime).getRuntime().exec(...)`, constructor invocation `new java.lang.ProcessBuilder(...)`, and bean references. So building a SpEL string from user input — or letting users supply the template — is an injection/RCE risk (this is the class of bug behind several Spring CVEs). Defenses, in order: (1) **don't** interpolate untrusted data into an expression string — pass it as a variable/root instead, keeping the expression static; (2) if the expression itself must be dynamic, evaluate with **`SimpleEvaluationContext`** (`forReadOnlyDataBinding()` or `forPropertyAccessors(...)`), which strips `T()`, constructors, and bean resolution; (3) keep expressions in trusted config, not in request payloads; (4) validate/allowlist. Also note SpEL appears in `@PreAuthorize` and query annotations — keep user data as method parameters (`#id`) rather than concatenating it into the expression text.
code
java · 17 linesExpressionParser parser = new SpelExpressionParser();
// UNSAFE: attacker controls the expression text -> RCE possible
String evil = "T(java.lang.Runtime).getRuntime().exec('rm -rf /')";
// parser.parseExpression(evil).getValue(new StandardEvaluationContext()); // DON'T
// SAFE pattern 1: fixed expression, untrusted value as a variable
Expression fixed = parser.parseExpression("score > #threshold");
StandardEvaluationContext ctx = new StandardEvaluationContext(record);
ctx.setVariable("threshold", userSuppliedInt); // typed data, not syntax
boolean pass = Boolean.TRUE.equals(fixed.getValue(ctx, Boolean.class));
// SAFE pattern 2: dynamic text must be evaluated -> restricted context
EvaluationContext safe = SimpleEvaluationContext
.forReadOnlyDataBinding()
.build(); // T(), new, and @beanName are disabled here
Object result = parser.parseExpression(dynamicButSandboxed).getValue(safe, data);go deeper
Recognize that evaluating user-provided expressions is dangerous and you should not build SpEL from user input.
Name the T()/new/@ vectors and know SimpleEvaluationContext exists as the safe option.
Explain the data-as-variable vs expression-as-text distinction, what SimpleEvaluationContext disables, and safe @PreAuthorize/@Query patterns.
Own the org policy: static trusted expressions only, SimpleEvaluationContext for any dynamic text, redesign toward sandboxed DSLs, and treat SpEL injection as a critical-severity class with defense-in-depth.
## Why SpEL is dangerous SpEL, evaluated with the default `StandardEvaluationContext`, exposes almost the entire JVM: - **`T()` type access** → any static method: `T(java.lang.Runtime).getRuntime().exec('...')`. - **Constructors** → `new java.lang.ProcessBuilder('sh','-c','...').start()`. - **Bean references** (`@beanName`) → reach into application beans. - **Method invocation** on any reachable object. If an attacker controls the *expression string*, they control code execution. This is the root cause of a family of real Spring vulnerabilities (SpEL injection). The danger is not evaluating SpEL per se — it is evaluating **attacker-influenced expression text** with a **powerful context**. ## Two failure modes 1. **Concatenating user input into an expression** — e.g. `parser.parseExpression("user." + requestParam)`. The parameter becomes executable syntax. 2. **Letting users supply whole templates** — e.g. a 'formula' field stored and later evaluated. ## Defenses (layered) ### 1. Keep the expression static; pass data as variables The expression string should be a **constant** in your code. Feed user data through the **root object** or **variables** (`#param`), never by string concatenation: ```java Expression e = parser.parseExpression("user.age > #minAge"); // fixed text ctx.setVariable("minAge", untrustedButTypedInt); ``` Data-as-variable can't change the grammar, so it can't inject code. ### 2. Use a restricted context for dynamic expressions If the expression *text* genuinely must be dynamic, evaluate with **`SimpleEvaluationContext`** instead of `StandardEvaluationContext`: - `SimpleEvaluationContext.forReadOnlyDataBinding().build()` — property reads + basic operators only. - `SimpleEvaluationContext.forPropertyAccessors(...).withConversionService(...).build()` — customize. It **disables `T()` type references, constructor calls, and bean (`@`) resolution**, closing the RCE avenues while still allowing safe property navigation. ### 3. Don't source expressions from untrusted channels Expressions belong in **trusted configuration** (code, vetted config files) — not request bodies, query strings, or user-editable records. If a feature requires user formulas, prefer a purpose-built, sandboxed mini-language, not raw SpEL. ### 4. Validate / allowlist Where dynamic text is unavoidable, allowlist permitted identifiers/operators and reject anything containing `T(`, `new`, `@`, etc. Treat this as defense-in-depth, not the primary control. ## In annotations `@PreAuthorize`, `@PostAuthorize`, `@Query` (Spring Data), `@Cacheable` all take SpEL. The safe pattern is to reference **method parameters** (`#id`, `#user`) and the security `principal`/`authentication` — never build the annotation string from runtime user input (annotations are compile-time constants anyway, but the same principle applies to any dynamically assembled expression). ## Compiled SpEL `SpelCompilerMode` (IMMEDIATE/MIXED) compiles expressions to bytecode for speed. It does **not** add sandboxing — a compiled expression from a full context is just as dangerous. Compilation is a performance feature, not a security one. ## Summary heuristic - Untrusted **data** + trusted **expression** → fine (use variables/root). - Untrusted **expression** → only with `SimpleEvaluationContext`, ideally never. - Untrusted **expression** + `StandardEvaluationContext` → critical RCE. ## When to use Use SpEL freely for internal/trusted config. The moment any part of the expression *text* could be influenced externally, switch to `SimpleEvaluationContext` or redesign to keep user data as variables.
- What exactly does SimpleEvaluationContext restrict compared to StandardEvaluationContext?It removes type references via T(), constructor invocation (new), and bean references (@name), and by default supports read-only data binding with a limited set of property accessors and operators. StandardEvaluationContext allows all of those, giving near-full JVM reach.
- Does compiling SpEL expressions make them safer?No. SpelCompilerMode (IMMEDIATE/MIXED) only improves evaluation performance by emitting bytecode. The available operations are still determined by the EvaluationContext; a compiled expression from a StandardEvaluationContext is exactly as dangerous.
- A feature needs user-defined filter formulas. How would you design it?Prefer a purpose-built, sandboxed grammar or a structured query DSL rather than raw SpEL. If SpEL is unavoidable, evaluate only with SimpleEvaluationContext, allowlist identifiers, forbid T()/new/@, and never run it in a StandardEvaluationContext; keep the formulas in a trust boundary you control.
saying these in an interview costs you the question
- Believing SpEL is inherently safe to run on user input
- Thinking passing user data as a variable is as risky as concatenating it into the expression
- Claiming compiled SpEL is sandboxed
- Using StandardEvaluationContext for user-supplied expressions
- Assuming validation/allowlisting alone fully mitigates SpEL injection