GHSA-4j8x-x6v7-w9rq Flowise: RCE via CSVAgent csvFile data URI base64 segment is interpolated into Python source without validation
SummaryFlowise'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):The captured `base64String` is then **interpolated verbatim** into a Python
source string at lines 156–171:**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):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:…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 `flowise@3.1.2` running on Node
20.20.2 — see [Verified end-to-end against live Flowise](#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.PoCVerified end-to-end on the cloned repo (commit
`a3ffe6611b0986d646b9cd8bb8787d4fdcf9be6d`, the same commit the prior audit
was based on).Reproducer setupTwo 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`:****`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(',')`.What gets assembledAfter the two `pop()` calls in `CSVAgent.ts:135-137` extract the third comma-separated segment, the Python text passed to `pyodide.runPythonAsync` becomes (note that Python's lexer resolves the `,` escapes inside the string literal back to commas, so the JS code actually receives `fs.writeFileSync('proof', 'marker')`):The `";` closes line 161's string literal; the injected statements execute
(awaiting the JS Promise that writes the proof file); the trailing `#`
comments out the dangling `"` so the rest of the bootstrap parses. The
remaining `b64decode("")` returns `b''` and `pd.read_csv` (in the live
template) then raises `pandas.errors.EmptyDataError`, but the
`fs.writeFileSync(...)` call has already fired in the Node host.Observed output (after deleting any prior proof file)The proof file `flowise_a1_pyodide_proof.txt` is written by the Node host
process via the Pyodide `js` bridge → `js.eval(...)` →
`(await import('fs')).writeFileSync(...)`, confirming the escape from the
Pyodide WASM sandbox. The standalone repro omits `import pandas`, so no
post-injection exception is raised — but the live template (`pandas.read_csv`
on the empty buffer) throws `pandas.errors.EmptyDataError` *after* the host
write has already happened, which is exactly the symptom an operator sees in
the chat panel.Verified end-to-end against live FlowiseThe standalone repro above proves the validator-bypass + sandbox-escape
primitive in isolation. The same payload was additionally verified against a
stock `flowise@3.1.2` install on Node 20.20.2:| Step | Action |
|---|---|
| 1 | `npm install -g flowise` (Node 20.20.2, Linux x64) |
| 2 | `flowise start` → bind on `:3000` |
| 3 | UI: create admin + dummy OpenAI credential (any string for the API key — never validated; the exploit fires before the LLM is invoked) |
| 4 | Plant the attached `evil-csvagent-flow.json` in the chatflows DB (UI import or `POST /api/v1/chatflows`) |
| 5 | Open the chatflow → click chat → send any message |
| 6 | Chat panel shows `pandas.errors.EmptyDataError: No columns to parse from file` |
| 7 | `/home/<user>/flowise_a1_proof.txt` is now present, 46 bytes, content `FLOWISE_A1_HOST_RCE_via_pyodide_dynamic_import`, owner-uid matches the Flowise process uid |Reproduction artifacts (`evil-csvagent-flow.json`, `build-flow-v2.js`,
`test-flow.js`, the captured `evidence-bundle.txt`) live at
`pocs/S1-csvagent-csvfile-rce/triage-response/`. The chatflow JSON is built
verbatim from Flowise's bundled `marketplaces/chatflows/CSV Agent.json`
template with three minimal edits — the malicious `csvFile` data URI on
`csvAgent_0`, a placeholder credential on `chatOpenAI_0`, and the sticky
note removed — so it imports cleanly into any Flowise 3.x without the
`reactFlowNodeData.inputParams.find(...)` 500 the maintainer initially saw
when handed a hand-crafted minimal flow.End-to-end against a live Flowise instanceThe local PoC above proves the validator-bypass + sandbox-escape primitive.
To reach the same primitive over HTTP against a deployed Flowise, two
requests suffice:Step 1 is the only authenticated step; Step 2 is unauthenticated when
`chatflow.apikeyid` is unset (the default for newly created chatflows).Impact**Class:** Remote Code Execution via Python-template injection escaping the
Pyodide sandbox through the `js` bridge.**Affected:** every Flowise deployment that exposes a chatflow containing a
`CSVAgent` node where `csvFile` is operator-supplied (i.e., overridable via
`nodeOverrides` for the API caller, or planted by any user with chatflow
edit permission).**Prerequisites:** one user with `chatflows:create` / `chatflows:update` /
`agentflows:create` / `agentflows:update` to plant the chatflow once. The
trigger is unauthenticated when the chatflow has no `apikeyid` set (the
default for newly created chatflows).**Result:** arbitrary OS-command execution as the Flowise process. Direct
access to Flowise's encrypted-credentials key file, the entire database,
the host filesystem, and any network resource the host can reach.Metadata**Affected versions:** Confirmed at commit
`a3ffe6611b0986d646b9cd8bb8787d4fdcf9be6d` (main, 2026-04-28) and at
`flowise@3.1.2`. The vulnerable code (`splitDataURI.pop()` + template-string
interpolation) appears unchanged across this range. Earlier 3.x versions
with the same data-URI parsing pattern are also believed to be affected,
but I did not verify each historical tag.**Fixed version:** Unpatched at the audited commit.**CVSS v3.1:**
`CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H` → Base score **9.9
(Critical)**.AV:N — public `/api/v1/prediction/:id` trigger.AC:L — deterministic; no race / timing.PR:L — one user with `chatflows:create` (or equivalent) plants the
chatflow. In OSS deployments, any registered user typically has this.UI:N — no user interaction required at trigger time.S:C — Pyodide's WASM/Python sandbox is the intended security authority
for this code path; the `js` bridge escape and the validator bypass break
out to the Node host process.C:H / I:H / A:H — full host compromise.**CWE:** CWE-94 (Improper Control of Generation of Code: 'Code Injection');
more specifically CWE-95 (Improper Neutralization of Directives in
Dynamically Evaluated Code: 'Eval Injection').Remediation**Maintainer fix (preferred — eliminates string-interpolation entirely):**
pass the base64 value through Pyodide's `globals.set` API instead of
template-string interpolation. In `packages/components/nodes/agents/CSVAgent/CSVAgent.ts`,
replace the construction at lines 156–171 with something like:This keeps the value as a Python `str` object that never enters the source
text. Apply the same change to `AirtableAgent.ts` if it follows the same
pattern.**Defense in depth (recommended as well):**Validate `base64String` against `^[A-Za-z0-9+/=]*$` before interpolation
(rejects every escape character used in the PoC).Disable Pyodide's `js` module on load. Pyodide supports `loadPyodide({ jsglobals: {} })`
or the `js`-module-removal recipe; either prevents the bridge to
`globalThis.process` on Node.js. Apply in
`packages/components/nodes/agents/CSVAgent/core.ts:LoadPyodide`.Run `validatePythonCodeForDataFrame` (or a stricter equivalent) over the
bootstrap template, not only over the LLM-emitted code. The current
ordering inverts the trust assumption.Add a positive allow-list to `validateCustomReadCSVFunction` enumerating
only safe pandas readers (e.g., `read_csv` and column-typed forms);
exclude `read_pickle`, `read_html`, `read_xml`, `read_parquet`,
`read_orc`, `read_feather`, `read_json` (these are independently
exploitable — see S2/S3 in the submission roadmap).**User mitigations until a patch ships:**Set `chatflow.apikeyid` on every chatflow that uses CSVAgent so
`validateFlowAPIKey` enforces auth on `/api/v1/prediction/:id`.Set `chatbotConfig.allowedOrigins` to a strict list (note: this only
defends against browser callers, not curl/server-side).Restrict `chatflows:create` / `agentflows:create` permissions to trusted
users only.Where possible, strip `csvFile` from the `nodeOverrides` allow-list on
affected chatflows so it cannot be supplied at prediction time.
CVSS v4.0 score: 9.4