Expression Language (EL) Injection
Many apps let admins, power-users, or internal tools write expressions like âprice * taxâ or âif user is VIP then âŠâ. An Expression Language (EL) is a mini-language used to evaluate those expressions against data.
What I see in reviews: these issues usually slip through because everything looks âfineâ in happy-path testing â until one weird request hits production.
EL Injection happens when the application evaluates an expression that contains untrusted input. The risk is not âa weird stringâ â itâs that the system starts treating attacker-controlled text as code-like logic.
Why EL injection exists (root cause)
- Convenience features: âcustom formulasâ, âdynamic routing rulesâ, âtemplate conditionsâ, âfeature flagsâ, ârules enginesâ.
- Unsafe evaluation: frameworks/libraries evaluate strings at runtime (e.g., expression parsers, rule engines, template engines).
- Overpowered context: the evaluation context exposes sensitive objects (request, headers, env, secrets, database helpers).
- Trust boundary mistake: mixing untrusted user input into the expression string (instead of passing it as a value).
Mental model: âexpression engine = interpreter with a contextâ
Think of EL evaluation as running an interpreter with two things:
- The expression string (what gets interpreted)
- The context (variables/functions available to the expression)
EL injection happens when Someone can control the expression string or meaningfully alter it, and the context is powerful enough to cause harm.
Vulnerable vs secure patterns (Node.js examples)
Vulnerable pattern: building an expression string
// Node/Express (example)
import { Parser } from "expr-eval"; // any expression engine / rules library
const parser = new Parser();
app.post("/pricing/preview", (req, res) => {
// â Risk: mixing untrusted input into the expression string
// Example business intent: "basePrice * (1 - discount)"
const base = Number(req.body.basePrice);
const discount = String(req.body.discount); // user-controlled
const expr = `basePrice * (1 - ${discount})`; // â untrusted in expression string
const value = parser.evaluate(expr, { basePrice: base });
res.json({ value });
}); Secure pattern: keep user input as data, not expression
import { Parser } from "expr-eval";
const parser = new Parser();
// â
Expression is server-owned / allow-listed
const PRICING_FORMULAS = {
"standard": "basePrice * (1 - discountRate)",
"vip": "basePrice * (1 - discountRate) - vipBonus"
};
function safeNumber(x, fallback = 0) {
const n = Number(x);
return Number.isFinite(n) ? n : fallback;
}
app.post("/pricing/preview", (req, res) => {
const formulaKey = String(req.body.formula || "standard");
const expr = PRICING_FORMULAS[formulaKey] || PRICING_FORMULAS.standard;
// User inputs are values in a constrained schema
const ctx = {
basePrice: safeNumber(req.body.basePrice, 0),
discountRate: Math.min(Math.max(safeNumber(req.body.discountRate, 0), 0), 1),
vipBonus: safeNumber(req.body.vipBonus, 0)
};
const value = parser.evaluate(expr, ctx);
res.json({ value });
}); Where EL injection shows up (practical hotspots)
- Rules engines: discount rules, fraud scoring, feature flags, dynamic access rules.
- Reporting/BI: custom calculated fields and filters that are evaluated server-side.
- Workflow automation: âif condition then actionâ logic saved by users.
- Templating systems: dynamic templates or âemail previewâ features (expression + context).
- Authorization logic: evaluating policies from strings (high risk if user can influence).
Risk depends on âcontext powerâ
Two EL evaluation setups can look similar but have very different risk based on what the expression can access.
- Lower risk: pure math expressions with only numeric variables (still needs validation to avoid DoS).
- Higher risk: expressions that can call functions, access objects, or reference environment/config.
- Highest risk: expressions that influence security decisions (authz, routing, deserialization-like behavior).
Exploitation progression (attacker mindset)
This describes attacker thinking at a high level (no copy/paste instructions). Attackers usually try to answer three questions:
Phase 1: Find an evaluation sink
- Where does the app accept âformulasâ, âconditionsâ, âtemplatesâ, or ârulesâ?
- Is the evaluated string derived from user input or stored user content?
Phase 2: Map the context
- Which variables are available in the expression?
- Are helper functions or objects exposed (request/user/session/config)?
Phase 3: Look for impact levers
- Security decisions: can the result affect authorization, routing, or policy?
- Data exposure: can the expression reference sensitive fields?
- Integrity: can it mutate state (write operations) or trigger side effects?
- Availability: can complex expressions cause high CPU/memory (expression DoS)?
Tricky edge cases & conceptual bypass patterns
- Normalization mismatch: one layer decodes or transforms input before evaluation (encoding and escaping rules differ).
- âSafe modeâ illusions: engines that claim sandboxing but still expose powerful objects indirectly via the context.
- Over-broad context: passing entire request/user objects âfor convenienceâ instead of minimal variables.
- Policy injection: expressions used in authorization or filtering logic (subtle integrity issues, not just data exposure).
- Expression DoS: deeply nested operations or expensive functions causing performance degradation.
- Stored EL: expressions saved in DB and evaluated later by background jobs (wider blast radius).
- Multi-tenant leaks: shared expression templates reading data across tenant boundaries if context scoping is weak.
Safe validation workflow (defensive verification)
- Inventory: endpoints/features that accept formulas/conditions/templates.
- Trace data flow: identify if user input is concatenated into an expression string or stored and later evaluated.
- Map context: list exposed variables/functions; check whether request/config/DB helpers are in scope.
- Assess impact: does evaluation influence authz, routing, data filtering, or write actions?
- Check guardrails: input validation, allow-lists, sandboxing, timeouts, and rate limits.
- Prove safely: demonstrate âexpression evaluation presentâ using benign expressions and observe predictable output changes.
Defensive patterns & mitigations
1) Prefer server-owned expressions (allow-list)
- Store expressions in code or a controlled admin-only configuration.
- Let users select from predefined formulas instead of supplying their own.
2) Minimize the evaluation context
- Pass only the variables needed (numbers/strings), not whole objects.
- Avoid exposing request/session/config/env or database helpers into the expression scope.
3) Constrain language features
- Disable function calls if not needed; limit operators and nesting.
- Apply length limits and complexity limits (depth, tokens).
4) Add safety controls
- Timeouts / cancellation for expensive evaluation.
- Rate limiting for evaluation endpoints.
- Audit logs for expression creation/changes.
Confidence levels (how sure are you?)
- Low: you only see âformula-likeâ inputs, but canât confirm server-side evaluation or impact.
- Medium: you can confirm evaluation occurs, but context is minimal and impact is limited (still watch for DoS).
- High: evaluation is confirmed and untrusted input influences the expression or powerful context; or results affect security decisions.
Checklist (quick review)
- Do any features accept formulas/conditions/templates that are evaluated server-side?
- Is user input concatenated into an expression string (vs passed as a value)?
- Is the expression server-owned (allow-listed) or user-controlled?
- What objects/functions are in the evaluation context? Is it minimal?
- Do results influence authorization, routing, or data filtering?
- Are there limits (length/complexity/timeouts/rate limits) to prevent expression DoS?
- Are changes audited and restricted to privileged roles?
Remediation playbook
- Contain: disable or restrict user-controlled expressions; force safe presets.
- Fix root cause: remove concatenation of untrusted input into expression strings; pass user values via typed variables.
- Reduce context: strip powerful objects from scope; expose only minimal primitives.
- Constrain engine: limit operators/features; enforce length and complexity limits; add timeouts.
- Review usage: search codebase for expression evaluation sinks and templating/rule engines.
- Test: add regression tests ensuring user input cannot alter expression structure; validate DoS protections.
- Monitor: log expression evaluation errors/timeouts and alert on unusual spikes.
Interview-ready answers (60-second + 2-minute)
60-second answer
EL injection is when an app evaluates an expression that includes untrusted input, turning attacker-controlled text into code-like logic. I assess it by checking who controls the expression string, what the evaluation context exposes, and whether results affect security decisions. Fixes are policy-driven: keep expressions server-owned (allow-lists), pass user input as values not expression fragments, minimize context, and add limits like timeouts and complexity controls.
2-minute answer
I treat expression evaluation as an interpreter plus a context. If user input can shape the expression, or the context exposes powerful objects, you can get integrity, confidentiality, or availability impact. I inventory formula/template/rule features, trace data flow to see if input is concatenated into an expression string, then map what variables/functions are in scope. Iâm especially cautious if the expression result controls authorization, routing, or data filtering. Remediation is to use server-owned templates/formulas, restrict expression features, pass user inputs through typed variables, minimize context to primitives, and add guardrails like timeouts, rate limiting, auditing, and regression tests.
Interview Questions & Answers (Easy â Hard)
Easy
- What is EL injection?
A: Layman: Itâs when user input gets treated like a formula the server executes. Deep: The expression engine interprets strings with a context. If untrusted data can control the expression or meaningfully change it, you have a code-like injection risk. - How is EL injection different from SQL injection?
A: Layman: SQLi targets database queries; EL injection targets expression evaluation. Deep: Both are âdata becomes codeâ issues, but EL injection depends on the expression engineâs features and context, not the database parser. - What are typical business features that introduce EL evaluation?
A: Layman: Custom formulas and rules. Deep: Discounts, scoring rules, feature flags, workflow conditions, report calculated fields, and template-like systems. - What are the three things you check first?
A: Layman: Who controls the expression, what it can access, and what it affects. Deep: (1) expression source (server-owned vs user), (2) evaluation context (objects/functions), (3) impact levers (authz, data access, writes, DoS). - Whatâs the simplest safe fix?
A: Layman: Donât let users write expressions. Deep: Use allow-listed formulas/templates controlled by the server and pass user input as typed values, not expression fragments. - Why can âescapingâ be unreliable here?
A: Layman: Because expressions arenât plain text. Deep: Different engines have different grammars; escaping rules vary; normalization/decoding can reintroduce meaning. Policy (allow-lists + minimal context) is stronger than string escaping.
Medium
- Scenario: A âpricing formulaâ field is editable by store managers. Whatâs your risk analysis?
A: Layman: If they can change the formula, they can change business logic. Deep: Determine if itâs server-side evaluation, what context is exposed, and whether it can access sensitive variables. Add role restrictions, allow-list formulas, and constrain expression features with auditing. - Scenario: Users can create workflow conditions (âif X then Yâ). What do you look for?
A: Layman: Whether those conditions are executed as code. Deep: Check if conditions are evaluated by an expression engine, ensure the context is minimal, and verify the actions are still authorized (donât let expression results bypass permission checks). - Scenario: A filter in a report builder changes which records a user can see. Why is this high risk?
A: Layman: Because it affects data access. Deep: If expression results influence server-side filtering, a weak boundary or shared context can cause data exposure. Ensure filters are translated to safe queries with strong tenant scoping and allow-lists. - Follow-up: How do you prove EL evaluation exists safely?
A: Layman: Use harmless expressions and see predictable output changes. Deep: Validate evaluation without attempting dangerous operations; focus on evidence of interpretation, context variables, and impact mapping. - Follow-up: What guardrails do you add for availability?
A: Layman: Limits so expressions canât overload the server. Deep: Length/complexity limits, timeouts, rate limits, caching of compiled expressions, and monitoring for spikes in evaluation time/errors. - Follow-up: What is âoverpowered contextâ and why does it matter?
A: Layman: Itâs giving the expression too many tools. Deep: If the context includes request/session/config/db helpers, the expression can potentially read/alter more than intended. Reduce to primitives and safe helper functions only. - Scenario: Expression is stored in DB and evaluated by a nightly job. What changes in your threat model?
A: Layman: The impact can be broader and delayed. Deep: Stored expressions can affect multiple users/tenants and run with higher privileges. Add strict write controls, approval workflows, auditing, and isolation of job permissions.
Hard
- Scenario: Multi-tenant app uses tenant-configured rules to route requests. What could go wrong?
A: Layman: A tenant might break rules and affect others. Deep: If routing decisions come from evaluated expressions, weak scoping often leads to cross-tenant impact. Ensure tenant isolation in context, restrict rule capabilities, and validate rule outputs against safe allow-lists. - How do you distinguish EL injection from safe parameterization?
A: Layman: Safe systems pass values, not code. Deep: If user inputs are bound as typed variables in a server-owned expression/template, itâs safer. If user inputs alter expression structure or introduce operators/functions, itâs risky. - Follow-up: If business requires âcustom formulasâ, whatâs your secure design?
A: Layman: Give them limited building blocks. Deep: Provide a constrained DSL with allow-listed operators, no object access, no function calls unless safe, strict context primitives, compile-time validation, quotas, and approval/auditing for changes. - Follow-up: What do you log/monitor in production?
A: Layman: Strange or expensive formula activity. Deep: Expression creation/updates, evaluation errors/timeouts, latency percentiles for evaluation endpoints, and abnormal spikes per tenant/user. Correlate with feature usage and incident signals. - Scenario: Expression result is used to decide whether to allow an action (âif condition then allowâ). Why is this dangerous even if the engine is sandboxed?
A: Layman: Because it can bypass permissions. Deep: Even a safe sandbox can produce a âtrue/falseâ that changes authorization. Authorization must remain enforced by server-side policy checks; expressions may inform UI or scoring, not final access control. - Follow-up: How do you scale prevention across a large Node.js codebase?
A: Layman: Standardize and scan for unsafe patterns. Deep: Provide a shared safe evaluation wrapper, ban ad-hoc eval-like usage, add lint rules/code scanning for string-to-execution APIs, and centralize rule configuration with reviews and tests. - Scenario: A âtemplate previewâ feature evaluates expressions with user profile data. What privacy concerns come up?
A: Layman: It might expose data it shouldnât. Deep: Ensure the context only includes permitted fields, enforce field-level access controls, and prevent users from referencing arbitrary objects. Validate that preview runs under the requesterâs permissions and tenant scope.