Skip to content

Security: SpEL injection RCE in Expression Hash Variable — case-insensitive bypass of the "class" blacklist (fix in commit "Prevent RCE" is incomplete) #114

Description

@cybervuln2077

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-coreorg.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

  1. Source — a remote HTTP parameter is processed as a hash variable and routed to the exp plugin
    (AppUtil.processHashVariable(...)ExpressionHashVariable.processHashVariable(variableKey)).
  2. SinkprocessHashVariable:
    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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions