Summary
The ExpressionHashVariable plugin evaluates remotely-controlled input as a Spring Expression
Language (SpEL) expression. The hardening added in commit "Fixed: wflow-core - Expression Hash
Variable - Prevent RCE" (Dec 2025) blocks access to class / classLoader / getClass /
getClassLoader, type references, beans and constructors. However, the property blacklist is
case-sensitive while SpEL property access is case-insensitive, so the check can be bypassed with
a capitalised Class, restoring full Remote Code Execution.
Affected component
wflow-core — org.joget.apps.app.lib.ExpressionHashVariable
- Present on the current default branch (the hardened version introduced by the Dec 2025
"Prevent RCE" commit). Earlier versions had no filtering at all and are also affected.
Root cause (the bypass)
In getContext() the evaluation context installs a SecurePropertyAccessor:
public static class SecurePropertyAccessor extends ReflectivePropertyAccessor {
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
if ("class".equals(name) || "classLoader".equals(name)) { // case-sensitive check
return false;
}
return super.canRead(context, target, name);
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
if ("class".equals(name) || "classLoader".equals(name)) { // case-sensitive check
throw new AccessException("Access to class/classLoader is forbidden");
}
return super.read(context, target, name);
}
}
SpEL resolves property names case-insensitively (e.g. obj.Class reads the same getClass() property
as obj.class). The blacklist only compares against the lowercase string "class", so ''.Class
is not blocked and returns the Class object. From there, forName(...).getMethod(...).invoke(...)
reaches Runtime.exec(...). The SecureMethodResolver only blocks the method names getClass /
getClassLoader, not forName / getMethod / invoke / exec, so the chain completes.
Vulnerability chain
- Source — a remote HTTP parameter is processed as a hash variable and routed to the
exp plugin
(AppUtil.processHashVariable(...) → ExpressionHashVariable.processHashVariable(variableKey)).
- Sink —
processHashVariable:
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(variableKey);
Object result = exp.getValue(getContext()); // evaluated with the (bypassable) secure context
Proof of concept (non-destructive)
Using a capitalised Class to step around the lowercase "class" blacklist:
''.Class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('calc')
Delivered through any request that reaches the Expression Hash Variable (e.g. an #{exp....} /
#exp.<expression> hash variable in a remotely-influenced field). calc is a harmless placeholder;
a real attacker would substitute an arbitrary command. The expression is evaluated at
exp.getValue(getContext()), confirming code execution despite the filter.
Impact
Remote Code Execution on the Joget server, bypassing the existing "Prevent RCE" mitigation.
- Severity: Critical —
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8)
Suggested remediation
The deny-list approach is fragile; an allow-list is strongly preferred:
- Replace the custom
StandardEvaluationContext + deny-list with a SimpleEvaluationContext
(SimpleEvaluationContext.forPropertyAccessors(...) with only the property accessors you need),
which structurally disallows type references, T(...), constructors and arbitrary method calls —
rather than trying to enumerate forbidden names.
- If the deny-list must remain, make the comparisons case-insensitive (e.g.
"class".equalsIgnoreCase(name)) and block reflective entry points such as forName,
getMethod/getDeclaredMethod, invoke, getMethods, etc. — but note that name-based blocking
is hard to make complete.
- Treat hash-variable expressions reaching this plugin as untrusted; do not evaluate
remotely-influenced strings as SpEL.
Notes
This is a bypass of the mitigation added in the commit "Fixed: wflow-core - Expression Hash Variable
- Prevent RCE"; the underlying expression-evaluation feature is still reachable with attacker input.
Summary
The
ExpressionHashVariableplugin evaluates remotely-controlled input as a Spring ExpressionLanguage (SpEL) expression. The hardening added in commit "Fixed: wflow-core - Expression Hash
Variable - Prevent RCE" (Dec 2025) blocks access to
class/classLoader/getClass/getClassLoader, type references, beans and constructors. However, the property blacklist iscase-sensitive while SpEL property access is case-insensitive, so the check can be bypassed with
a capitalised
Class, restoring full Remote Code Execution.Affected component
wflow-core—org.joget.apps.app.lib.ExpressionHashVariable"Prevent RCE" commit). Earlier versions had no filtering at all and are also affected.
Root cause (the bypass)
In
getContext()the evaluation context installs aSecurePropertyAccessor:SpEL resolves property names case-insensitively (e.g.
obj.Classreads the samegetClass()propertyas
obj.class). The blacklist only compares against the lowercase string"class", so''.Classis not blocked and returns the
Classobject. From there,forName(...).getMethod(...).invoke(...)reaches
Runtime.exec(...). TheSecureMethodResolveronly blocks the method namesgetClass/getClassLoader, notforName/getMethod/invoke/exec, so the chain completes.Vulnerability chain
expplugin(
AppUtil.processHashVariable(...)→ExpressionHashVariable.processHashVariable(variableKey)).processHashVariable:Proof of concept (non-destructive)
Using a capitalised
Classto step around the lowercase"class"blacklist:Delivered through any request that reaches the Expression Hash Variable (e.g. an
#{exp....}/#exp.<expression>hash variable in a remotely-influenced field).calcis a harmless placeholder;a real attacker would substitute an arbitrary command. The expression is evaluated at
exp.getValue(getContext()), confirming code execution despite the filter.Impact
Remote Code Execution on the Joget server, bypassing the existing "Prevent RCE" mitigation.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H(9.8)Suggested remediation
The deny-list approach is fragile; an allow-list is strongly preferred:
StandardEvaluationContext+ deny-list with aSimpleEvaluationContext(
SimpleEvaluationContext.forPropertyAccessors(...)with only the property accessors you need),which structurally disallows type references,
T(...), constructors and arbitrary method calls —rather than trying to enumerate forbidden names.
"class".equalsIgnoreCase(name)) and block reflective entry points such asforName,getMethod/getDeclaredMethod,invoke,getMethods, etc. — but note that name-based blockingis hard to make complete.
remotely-influenced strings as SpEL.
Notes
This is a bypass of the mitigation added in the commit "Fixed: wflow-core - Expression Hash Variable