#5438·Activiti

EL Injection Guard Bypass via Deferred Expression Syntax (`#{}`) in MailActivityBehavior

Author: geo-chenCreated Jun 13, 2026Updated Jun 13, 2026

Summary

ProcessVariablesPayloadValidator blocks ${} EL expressions in process variable values to prevent expression injection. The guard pattern only matches ${} and silently allows #{} deferred expressions. JUEL (the bundled expression language implementation) evaluates both ${} and #{} identically at runtime. When a BPMN mail task uses the textVar or htmlVar feature, a process variable value that begins with #{ is retrieved and passed directly to ExpressionManager.createExpression(), where it is evaluated in the full Spring EL context.

Details

ProcessVariablesPayloadValidator.checkPayloadVariables() calls expressionResolver.containsExpression(value) before storing a variable. ExpressionResolver defines:

java
// activiti-core/activiti-api-impl/activiti-api-process-runtime-impl/.../ExpressionResolver.java
private static final String EXPRESSION_PATTERN_STRING = "([\\$]\\{([^\\}]*)\\})";
private static final Pattern EXPRESSION_PATTERN = Pattern.compile(EXPRESSION_PATTERN_STRING);

This pattern matches only ${...}. A value of #{someBean.someMethod()} returns false from containsExpression() and is accepted.

At runtime, MailActivityBehavior.getExpression() reads the stored variable and creates a live expression from it:

java
// activiti-core/activiti-engine/.../bpmn/behavior/MailActivityBehavior.java, line 410
protected Expression getExpression(DelegateExecution execution, Expression var) {
    String variable = (String) execution.getVariable(var.getExpressionText());
    return Context.getProcessEngineConfiguration().getExpressionManager().createExpression(variable);
}

The returned expression is then evaluated when building the email body (lines 81-86). JUEL's AstEval.eval() handles deferred expressions identically to dynamic ones:

java
// activiti-core-common/activiti-juel-jakarta/.../ast/AstEval.java, line 50
@Override
public Object eval(Bindings bindings, ELContext context) {
    return child.eval(bindings, context);  // deferred flag ignored at eval time
}

The Scanner explicitly recognizes #{ as START_EVAL_DEFERRED and the Parser builds an AstEval(child, deferred=true) node that evaluates identically to a ${} node.

In a Spring context, ApplicationContextElResolver exposes all Spring beans by name:

java
// activiti-core/activiti-spring/.../ApplicationContextElResolver.java, line 41
if (applicationContext.containsBean(key)) {
    context.setPropertyResolved(true);
    return applicationContext.getBean(key);
}

An attacker can reference beans such as processRuntime, taskRuntime, runtimeService, or any custom application bean directly in the injected expression. Direct RCE via Runtime.exec() is blocked by ELResolverReflectionBlockerDecorator, but bean method invocation is unrestricted.

PoC

(available upon request)

Impact

An authenticated user with ROLE_ACTIVITI_USER can store a #{} EL expression as a process variable value, bypassing the security control in ProcessVariablesPayloadValidator. When the process reaches a mail task configured with textVar or htmlVar pointing to the attacker-controlled variable, the expression is evaluated in the full Spring application context. This allows:

  • Invoking methods on any Spring bean visible in the application context (e.g., processRuntime, runtimeService, taskRuntime, custom service beans)
  • Reading process-engine internal state, listing process definitions, querying task assignments
  • Calling any Spring service method that does not require elevated credentials

Direct OS command execution via Runtime.exec() or ProcessBuilder requires class instantiation blocked by ELResolverReflectionBlockerDecorator, but bean-level method invocation is unrestricted. The exploitability depends on what Spring beans are registered in the deploying application, but any Spring Boot application using Activiti mail tasks with variable-body fields is affected.

Package: org.activiti:activiti-engine / org.activiti:activiti-spring-boot-starter Affected Versions: <= 7.1.0.M6 (Maven Central); <= 7.21.0-rc.331 / 9.1.0-alpha.31 (GitHub) CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N (6.5) CWE: CWE-917: Improper Neutralization of Special Elements used in an Expression Language Statement