import pandas as np, os as pandas
pandas.system("xcalc")
Those two lines are the whole bypass in CVE-2026-41264, a CVSS 9.8 bug in Flowise, the open source drag-and-drop builder for LLM apps. Flowise's CSV Agent asked a model to write pandas code, checked the code with a regex, and ran it on the server. The regex was meant to stop any import other than pandas or numpy. It looked at the first module name after import and stopped there. Alias os as pandas and the check passes.
The bug is worth studying because of what happened after it was reported. The fix in 3.1.0 was a stricter regex. Over the next three months the validator grew to 390 lines, picked up five more CVEs, and in June Flowise deleted the feature. If you build or test anything that runs model-generated code, that history is the lesson.
The data path
Trend Micro's Zero Day Initiative reported the bug as ZDI-CAN-29411. The researchers credited are Dre Cura and Nicholas Zubrisky of TrendAI Research. Their analysis is published in full in the Flowise advisory GHSA-3hjv-c53m-58jj. The flow inside CSV_Agents.run(), as the advisory quotes it:
- Load the uploaded CSV in Pyodide and extract column names and dtypes with pandas.
- Fill a prompt template with those dtypes and the user's question. The template ends with a "Security:" paragraph telling the model not to use
import,exec,eval,open,osorsubprocess. - Take the model's reply, strip Markdown fences, and store it as
pythonCode. - Run
validatePythonCodeForDataFrame(pythonCode), a loop over 34 forbidden regexes. - Execute
import pandas as pd\npluspythonCodewithpyodide.runPythonAsync().
Step 2 is a request to the model, not a control. Step 4 is the only real check, and step 5 is where it fails. An unauthenticated attacker reaches all of it because /api/v1/prediction/ sits on Flowise's WHITELIST_URLS. The ZDI proof of concept puts a prompt injection in the question field asking the model to reply with the payload above. The advisory says it was tested against Llama 3.2 in Ollama and can take several attempts, which is normal for anything that depends on model output.
There is also a path with no model at all. A logged-in user creates a chatflow whose ChatOllama node points at a server they control, and that server answers /api/chat with Python. The regex is the only thing between that reply and the interpreter.
The version range hides one detail. Steps 2 and 4 came from PR #5701, merged on 2026-02-06, three days after 3.0.13 shipped. The 3.0.13 release has no validator and no "Security:" paragraph: model output went straight to runPythonAsync(). ZDI's bypass beat the code headed for the next release. Anyone on 3.0.13 or earlier was exposed without the alias trick. The same node had a second bug, CVE-2026-41137 (CVSS 8.8): a custom read call set by the chatflow builder was pasted into Python as pd.${customReadCSVFunc} with no checks.
Why Pyodide did not contain it
Pyodide is CPython compiled to WebAssembly, and it is easy to assume that means isolation. Inside Node it does not. The Pyodide Python API docs describe the js module as "the global JavaScript scope." In a Node process that scope leads to child_process. CVE-2026-69255 describes exactly that chain: use Pyodide's js bridge to load child_process, then run commands as root in the Flowise container. WebAssembly limits memory access. It does not limit what the host hands the guest.
The fix that kept failing
Version 3.1.0, released 2026-03-16, was the first release with any validator. It shipped with a stricter import rule from PR #5879 (any use of the word import fails), four more reflection patterns, and the custom read field run through the same check. That closes the alias trick and CVE-2026-41137. The CVE records in our AI CVE index show what came next, all fixed in 3.1.3:
| CVE | CVSS | Bypass |
|---|---|---|
| CVE-2026-69256 | 8.8 |
pd.read_pickle() in the custom read field deserializes a payload without matching any forbidden word |
| CVE-2026-69255 | 8.8 | CSV contents interpolated into base64_string = "...", so a quote in the data breaks out |
| CVE-2026-70470 | 9.8 | Unicode homoglyphs in dunder names |
| CVE-2026-70477 | 9.8 | Prompt injection that gets model-written code past the denylist, the same path as CVE-2026-41264 |
| CVE-2026-73487 | 9.8 |
pd.read_json() left open, giving data exfiltration and SSRF |
The homoglyph bug is the one to remember. JavaScript's \b only knows ASCII word characters. Python normalizes identifiers with NFKC at parse time, as PEP 3131 specifies. Spell __class__ with MATHEMATICAL BOLD SMALL A (U+1D41A) in place of the a and the two languages disagree about what the string says:
/\b__class__\b/.test("__cl\u{1D41A}ss__") // false: validator passes it
import unicodedata
unicodedata.normalize("NFKC", "__cl\U0001D41Ass__") # '__class__'
exec("x = ().__cl\U0001D41Ass__") # x is <class 'tuple'>
The commit history reads the same way. Flowise blocked read_pickle and class definitions on 2026-04-27 (PR #6257), restricted the CSV field to a literal read_csv() call on 2026-05-05 (#6313), and added homoglyph and backslash line-continuation normalization on 2026-06-05 (#6476). On 2026-06-10, PR #6499 removed the CSV Agent, the Airtable Agent, the 390-line validator, and its 599-line test file. That shipped in 3.1.3 on 2026-06-25.
So NVD's "fixed in 3.1.0" is accurate for this CVE and misleading for your exposure. If a CSV Agent ran on 3.1.0 through 3.1.2, it was still exploitable.
If you run Flowise
Check the version first. npm ls -g flowise --depth=0 covers npm installs. For containers, docker ps --format '{{.Image}}' | grep -i flowise shows the tag. Anything below 3.1.3 with a CSV or Airtable Agent in use is exposed.
Then find the flows. The default SQLite store lives at ~/.flowise/database.sqlite, and the node names are csvAgent and airtableAgent:
sqlite3 ~/.flowise/database.sqlite \
"SELECT id, name, isPublic, apikeyid IS NULL AS no_key
FROM chat_flow
WHERE flowData LIKE '%csvAgent%' OR flowData LIKE '%airtableAgent%';"
Rows with no_key = 1 were reachable without credentials. Those flows break on upgrade, so warn their owners first.
To hunt, pull user messages sent to those chatflows and look for code, pandas I/O calls, and characters outside printable ASCII:
sqlite3 ~/.flowise/database.sqlite \
"SELECT createdDate, chatflowid, substr(content, 1, 200)
FROM chat_message
WHERE role = 'userMessage'
AND (content LIKE '%import%' OR content LIKE '%read_pickle%'
OR content LIKE '%read_json%' OR content LIKE '%\_\_%' ESCAPE '\'
OR content GLOB '*[^ -~]*');"
The last clause also matches ordinary newlines and non-English text, so expect noise. The authenticated path leaves no injection in chat_message. For that, review chat model nodes whose base URL points somewhere you do not recognize.
Process telemetry catches both paths. Exploitation shows up as T1190 followed by T1059.004: a node process spawning a shell. A Falco rule:
- rule: Shell or network tool spawned by Flowise
desc: Node process in a Flowise container started a shell or downloader
condition: >
spawned_process and proc.pname = node
and container.image.repository contains "flowise"
and proc.name in (sh, bash, dash, curl, wget, nc, python3)
output: "Flowise child process (cmd=%proc.cmdline parent=%proc.pcmdline image=%container.image.repository)"
priority: CRITICAL
On the ATLAS side, the injection is AML.T0051 and the code execution is AML.T0050, Command and Scripting Interpreter.
What the CSV Agent should have been
OWASP LLM05:2025 Improper Output Handling covers this class of bug: model output passed to an interpreter without being treated as untrusted. CVE-2026-41264 is a textbook case. The fix is the same as for any untrusted code. Run it in a separate process with no network, no credentials, a read-only copy of the data, and a seccomp or gVisor boundary. A denylist on Python source is a losing game. Python has too many ways to name the same object.
Isolation has limits too. A sandboxed interpreter can still return every row of the dataframe to whoever asked, and CVE-2026-73487's read_json SSRF is a reminder that egress needs blocking as well. Sometimes the right answer is the one Flowise reached: remove the feature.
None of the bypasses needed machine learning knowledge. An alias, a pickle, a Unicode quirk and an unescaped quote are standard application security findings. That is why the AI Red-Teaming course asks for security testing experience and not a data science background. When the LLM's output feeds an interpreter, testing it is the same as testing any other input validator.
Top comments (0)