把一个 Langflow Flow 发布成公开 MCP 或 API 入口,只能说明"调用者可以到达这个入口"。它不能继续推导出"匿名调用者可以继承流程拥有者的身份、秘密、代码执行能力、stdio 进程权限、任意网络出口和历史 Session"。公开性是访问策略,不是权限复制器;真正的授权必须在最终执行点,按调用者身份、秘密、代码、进程、网络和会话六个面分别判断。
这是本文的核心判断,也是今天最需要转成回归用例的工程动作。
Langflow v1.11.5 在 2026 年 8 月 25 日发布,把 1.12 线的多组安全修复回移到 1.11 维护分支。当天发布的 v1.12.0 已进入稳定线,但官方 Release 正文只明确描述了多文件原始内容输出修复,没有完整枚举全部安全变化。因此,本文不会把一个版本标签写成"安全问题已经全部解决",而是依据官方 Release、固定版本 Patch 和已核验的变更范围,抽出一套框架无关的执行门禁。
本次没有安装、升级或部署 Langflow,也没有对真实服务做攻击测试。文中的 Langflow 变化属于官方事实;六面门禁、回归矩阵和示例代码属于工程判断;Node.js v22.19.0 上的 7 项测试属于本地验证。三者不能混为"生产环境已验证"。

一、先纠正一个危险等式:公开入口 ≠ Owner 身份
可视化工作流很容易制造一种错觉:画布已经保存,节点也能跑,发布开关一开,外部调用不过是"换了个入口"。但运行时真正面对的是另一组问题:
- 谁发起这次请求,是登录用户、匿名主体、服务账号,还是被错误映射的流程拥有者?
- Flow 中保存的 Secret 是否会被匿名调用读取,或在导出、错误、Trace 中泄露?
- 自定义代码组件是否允许在公开入口执行?检查发生在保存时,还是发生在实际运行前?
- MCP 配置如果使用 stdio,谁可以启动本地进程,命令和环境变量由谁控制?
- 自定义 Provider URL 在解析、重定向和真实连接时是否仍指向允许的公网地址?
- Session 是按连接、项目和 Flow 隔离,还是匿名调用者会命中 Owner 的历史缓存?
如果系统只做一个 isPublic === true 判断,上述六个问题会被压缩成同一个布尔值。结果不是"逻辑更简单",而是某个入口的允许状态意外覆盖另一个能力面。
Langflow v1.11.5 的公开 MCP 修复体现了相反的方向:匿名调用使用独立主体,不应直接按流程拥有者身份运行;公开执行先拒绝代码组件、清理持久化秘密,并按 MCP 连接、项目和 Flow 隔离 Session Namespace。与此同时,Provider 网络出口、生成代码、MCP stdio 和秘密导出又分别补上各自的复验或清理路径。
这些变化说明,安全边界不在"发布"按钮,也不只在组件被创建或配置被保存的时刻。真正的边界位于副作用即将发生的地方。
二、六个面必须独立授权,不能互相代替
先把门禁拆开,后面的代码和测试才不会继续围绕一个万能的 allowed 打补丁。
| 授权面 | 最小问题 | 失败时应阻止什么 | 可回读证据 |
|---|---|---|---|
| 调用者身份 | 谁在调用,是否错误继承 Owner | 所有后续执行 | 规范化主体 ID、认证类型、租户与 Flow 所属关系 |
| 秘密隔离 | 公开入口能否读持久 Secret | Secret 注入、导出与日志泄漏 | 清理后的组件值、Secret 元数据、脱敏审计 |
| 代码组件 | 当前入口是否允许执行自定义代码 | 生成代码、代码组件与动态导入 | 组件类型、策略版本、拒绝原因 |
| 进程策略 | stdio 连接是否允许真正拉起进程 | 命令、环境变量和本机子进程 | 连接前策略结果、命令摘要、管理员策略版本 |
| 网络出口 | 最终连接 IP 是否仍在允许范围 | SSRF、回环、私网和元数据访问 | 规范化 URL、DNS 结果、固定 IP、每次重定向结果 |
| 会话隔离 | Session 是否绑定连接、项目与 Flow | 跨租户上下文、缓存和状态串用 | Namespace、连接 ID、项目 ID、Flow ID |
这六面有依赖关系,但不是一个大开关。调用者身份是第一层,因为后续策略都需要知道"谁";秘密、代码与进程决定这次运行能获得什么本地能力;网络决定这些能力可以连到哪里;会话隔离决定这次调用可以继承哪些历史状态。
例如,一个认证用户可以调用公开 Flow,不代表他可以启动任意 stdio Server;管理员允许某个 stdio 配置,也不代表这个进程可以访问 loopback Provider;Provider URL 通过网络校验,也不代表匿名主体可以带上 Flow Owner 的 API Key。任何一面通过,都不能替另一面签字。
三、第一步:把入口身份和资源所有权分开
最小动作不是给匿名用户补一个名字,而是建立不可歧义的主体模型。至少区分:
owner:Flow、组件和持久资源的拥有者;caller:发起当前请求的登录或匿名主体;tool:实际执行某个工具或连接的服务身份。
运行上下文不要只保留 userId。同名字段很容易在恢复、队列投递和嵌套工具调用中被覆盖。可以显式保存:
json
{
"flowOwnerId": "user-owner-1",
"caller": {
"id": "anonymous-17",
"kind": "anonymous",
"tenantId": null
},
"toolPrincipal": {
"id": "svc-provider-readonly",
"scope": ["provider:invoke"]
}
}
公开入口应创建新的匿名或访客主体,而不是把 caller.id 填成 flowOwnerId。如果业务确实需要 Owner 代理执行,也应把它定义为单独的、可审计的委托关系,限制工具、资源、期限和可撤销性,而不是借"公开 Flow"隐式完成委托。
**过关证据:**同一个 Flow 分别被 Owner、登录访客和匿名主体调用时,Trace 中出现三个不同 Caller;工具权限由 Caller 与 Tool Principal 共同决定;任何请求都不能仅凭 flowId 还原成 Owner 权限。
**适用边界:**这里解决的是身份混淆,不解决 Secret 泄漏和代码执行。主体区分正确后,仍要继续通过后五面门禁。
四、第二步:在公开运行副本中移除秘密和代码能力
不少系统只按字段名清理秘密:看到 api_key、token 就删,看到 password 或普通命名的连接字符串却可能遗漏。Langflow 新修复采用组件元数据驱动的 Secret Scrubber,这个方向比字符串猜测可靠,因为"是否敏感"来自字段定义,不来自字段名长得像不像密钥。
建议把公开执行准备拆成一个纯函数:输入是持久化 Flow,输出是公开运行副本与拒绝原因。这个函数至少完成三件事:
- 根据组件 Schema / 元数据清理所有 Secret 字段;
- 发现自定义代码、生成代码或管理员限定组件时失败关闭;
- 保留原始 Flow 不变,避免一次匿名调用反向污染 Owner 的编辑态。
text
持久化 Flow
→ 深拷贝为运行副本
→ 按组件元数据清理 Secret
→ 扫描代码与管理员限定组件
→ 生成公开执行计划或结构化拒绝
只在导出文件时做清理还不够。运行时、调试输出、Trace、异常消息和重试队列都可能拿到组件值。清理应发生在公开执行上下文形成之前;观测侧还要再做一次最小采集与脱敏,避免安全门禁自己成为秘密的第二个落点。
**过关证据:**使用名称分别为 api_key、password、credential 和普通业务名的四类 Secret 字段导出并运行公开 Flow,所有敏感值都不可见;代码组件返回稳定的策略拒绝,而不是进入 Python / JavaScript 解释器后才报错。
**回滚条件:**如果升级后合法公开 Flow 因组件元数据缺失被拒绝,不要把清理策略改回字段名匹配。先冻结公开入口,补齐组件元数据和迁移脚本,再灰度恢复。
五、第三步:注册时通过,不代表 stdio 连接时仍可执行
MCP stdio 配置会启动本地进程。它与普通 HTTP 地址不是同一种风险:命令、参数、工作目录、环境变量和继承权限都会变成宿主机副作用。
常见遗漏是只在"新增 MCP Server"或"导入配置"时检查管理员策略。配置随后进入数据库或 Bundle;真正连接时,运行时把它当作已经批准的对象直接使用。如果策略、角色、配置内容或宿主环境已经变化,旧检查就失去意义。
正确的检查点靠近 spawn:
text
读取已保存配置
→ 规范化 command / args / cwd / env
→ 重新加载当前管理员策略
→ 绑定当前 Caller 和 Flow
→ 允许后才创建 stdio 进程
这里的"重新检查"不是重复做无意义工作。保存时检查保护配置仓库,连接前检查保护真实执行。两者面对的时间、主体和副作用不同。
连接前记录的审计信息也要克制:可以记录命令基名、参数数量、策略版本和拒绝码,不要把完整环境变量或可能含 Token 的参数原样写入日志。
**过关证据:**导入一个当时允许、随后被管理员禁用的 stdio 配置;再次连接必须在进程创建前失败,并确认没有子进程 PID、没有网络请求、没有工具列表返回。
**适用边界:**该门禁不替代容器、系统账户、沙箱与文件权限。它只保证策略拒绝发生在 spawn 之前;即使允许,也应继续限制进程实际能力。
六、第四步:Provider URL 要校验真实连接,不只校验字符串
自定义 Provider Base URL 通常同时携带 API Key。一次 SSRF 不只意味着"服务器可以访问内网",还可能让凭据被发往攻击者控制的地址。
字符串层校验只能拦住明显的 127.0.0.1、localhost 或私网字面量。域名可以先解析到公网地址,通过校验后再解析到回环或私网,这就是 DNS rebinding 的典型窗口。重定向也会把最初的公开地址带到内部目标。
Langflow v1.11.5 在 Anthropic 和 vLLM Multivector 路径中引入重校验、DNS 结果固定的 Client / Request Helper,并要求显式 allowlist 才能访问 loopback。工程上可以把网络门禁拆成五步:
- 规范化 scheme、hostname、端口和 URL Authority;
- 拒绝 userinfo、歧义 Authority 与不允许的协议;
- 解析全部 A / AAAA 结果,拒绝私网、回环、链路本地和云元数据地址;
- 把校验结果固定到实际连接,避免连接阶段重新解析到另一地址;
- 对每次重定向重新执行同样检查。
显式 allowlist 应表达"这个确切目标是业务需要",而不是"所有 loopback 都可以"。本地开发可以允许指定主机和端口,生产默认仍应拒绝回环。代理、Service Mesh 和自定义 DNS 会改变真实连接路径,必须纳入回归。
**过关证据:**测试字面量回环、IPv6 回环、十进制 / 十六进制变体、首次公网后回环的双解析、公开地址到私网的 30x 跳转,以及合法 Provider。日志能回读最终固定 IP 和拒绝阶段,但不记录 Authorization Header。
**回滚条件:**如果 DNS 固定与现有代理或连接池不兼容,先关闭自定义 Provider 出口或限定已知域名,不要临时恢复"只校验 hostname"。
七、第五步:Session Namespace 必须包含连接、项目和 Flow
公开入口经常为了性能复用 Session、缓存或对话历史。如果 Namespace 只使用 flowId,同一个公开 Flow 的匿名访问者可能共享上下文;如果使用 Owner ID,匿名请求又可能命中拥有者的历史状态。
一个可检查的最小 Namespace 可以是:
text
projectId:flowId:connectionId
有租户和调用者时可以继续扩展,但不要用浏览器可随意伪造的显示名。连接断开、身份升级、Flow 版本变化和策略变化时,是否沿用旧 Session 也要有明确规则。
Session 隔离不只影响聊天记录。工具批准、缓存的 Provider Client、临时文件引用和恢复检查点都可能附着其上。把所有状态放进同一个不分主体的全局缓存,即使前面五面都正确,仍可能通过历史对象绕回旧权限。
**过关证据:**两个匿名连接调用同一个 Flow,Namespace 不同;同一连接恢复时 Namespace 稳定;切换项目或 Flow 后必然改变;清理一条连接不会删除其他连接的状态。
**适用边界:**Namespace 只提供逻辑隔离。跨租户强边界还应由数据库行级策略、独立存储前缀、加密密钥和资源配额共同保证。
八、本地 7 项实验:拒绝必须发生在副作用之前
为了验证六面门禁的组合方式,本次用 Node.js 标准库写了一个框架无关的纯函数。它不连接 Langflow、不读取 API Key、不访问网络,也不启动真实进程;目的只是检查失败分类与"先拒绝、后副作用"的顺序。
js
function authorizeExecution(input) {
const failures = [];
if (!input.subject?.id || input.subject.kind === 'owner-shadow') {
failures.push('CALLER_IDENTITY');
}
if (input.publicEntry && input.flow.hasPersistentSecrets) {
failures.push('SECRET_ISOLATION');
}
if (input.publicEntry && input.flow.hasCodeComponent) {
failures.push('CODE_COMPONENT_POLICY');
}
if (input.transport === 'stdio' && !input.adminPolicy.allowStdio) {
failures.push('PROCESS_POLICY');
}
if (!input.network.resolvedIps.every(
(ip) => input.network.allowedIps.includes(ip)
)) {
failures.push('NETWORK_POLICY');
}
const expected = `${input.projectId}:${input.flowId}:${input.connectionId}`;
if (input.session.namespace !== expected) {
failures.push('SESSION_ISOLATION');
}
return failures.length
? { allowed: false, failures }
: { allowed: true, failures: [] };
}
测试基线包含一个合法公开 Flow,再逐项修改一个授权面。实际命令使用本机 node,输出如下:
text
{"name":"valid public flow","allowed":true,"failures":[]}
{"name":"owner shadow rejected","allowed":false,"failures":["CALLER_IDENTITY"]}
{"name":"persistent secret rejected","allowed":false,"failures":["SECRET_ISOLATION"]}
{"name":"code component rejected","allowed":false,"failures":["CODE_COMPONENT_POLICY"]}
{"name":"stdio rejected at connect","allowed":false,"failures":["PROCESS_POLICY"]}
{"name":"rebound ip rejected","allowed":false,"failures":["NETWORK_POLICY"]}
{"name":"session namespace rejected","allowed":false,"failures":["SESSION_ISOLATION"]}
{"node":"v22.19.0","passed":7,"failed":0}
7 项测试全部通过、0 项失败。这个结果只能证明示例函数按预期分类,不能证明 Langflow 真实部署已经安全。生产实现还必须把 resolvedIps 绑定到真实 Socket,把 stdio 拒绝放到 spawn 前,把 Secret 清理接到组件元数据,并把 Namespace 传入实际 Session Store。
九、把检查贴到副作用前:五个常见漏检现场
"最终执行点复验"不是一句抽象原则,它要求团队把检查位置画到调用链上。下面五种实现表面上都有安全代码,却仍可能在最后一步失守。
第一种是路由层已经鉴权,恢复任务却直接进入执行器 。HTTP 请求经过登录校验后创建了任务,几分钟后 Worker 从队列恢复;如果 Worker 只信任序列化的 userId,没有重新加载当前主体、租户和策略,账号停用、角色变更或错队列都可能沿用旧权限。恢复点应重新构造安全上下文,并把原请求身份当作待验证输入,而不是可信事实。
第二种是组件保存时已经扫描,运行时却使用导入后的新配置。Bundle、模板和复制 Flow 会改变组件值或来源。导入过程即使检查过一次,运行计划生成前仍要按当前组件元数据检查 Secret 和代码能力。过关证据不是"导入成功",而是公开运行副本中没有敏感值,解释器也没有被创建。
第三种是MCP Server 已登记在白名单,连接参数却可以被 Flow 覆盖 。白名单批准的可能是命令 A,实际 spawn 前参数被节点输入、环境变量或旧缓存改成命令 B。策略应对规范化后的最终 command、args、cwd 和允许的环境变量集合签字。只比较 Server 名称,等于批准了一个可变别名。
第四种是Provider 域名通过校验,HTTP Client 又重新解析 DNS。校验阶段得到公网 IP,连接阶段如果让系统解析器再次查询,校验与使用之间仍有竞态。需要把已验证结果固定到实际连接,并处理 TLS SNI、Host Header、连接池复用和重定向;否则日志里虽然有"SSRF check passed",Socket 仍可能连到另一地址。
第五种是Session Key 隔离正确,但缓存值里保留了旧权限对象。Namespace 不冲突只能保证键不同,不能保证值安全。Provider Client、工具列表、批准票据和临时文件句柄如果在策略升级后继续复用,新的调用仍可能带着旧能力。缓存项至少绑定主体、策略版本、Flow 版本和失效时间;安全策略提高时,应主动失效旧缓存,而不是等待自然过期。
这五个现场有同一条验收语句:检查通过后到副作用发生前,任何可影响授权的数据都不能再次变化;如果会变化,就在最后一个变化点之后重新检查。
十、不要只测成功路径:最小回归矩阵这样排
升级到 v1.11.5 或 v1.12.0 后,只打开画布跑一次成功请求,覆盖不到这次变化的核心。至少交叉测试以下维度:
| 维度 | 正常样本 | 拒绝样本 | 必查证据 |
|---|---|---|---|
| 主体 | 登录 Caller | 匿名误继承 Owner | Caller / Owner / Tool 三种身份分离 |
| 入口 | 私有 HTTP | 公开 MCP、恢复入口 | 每个入口都重新准备安全上下文 |
| 组件 | 普通模型节点 | 代码组件、生成代码逃逸 | 解释器或动态导入前拒绝 |
| 传输 | HTTP MCP | 被策略禁用的 stdio | 无子进程、无工具列表 |
| 网络 | 固定公网 Provider | loopback、私网、DNS 重绑定、重定向 | 实际连接 IP 与每跳检查 |
| 秘密 | 公开参数 | 任意命名的 Secret 字段 | 运行、导出、日志三处都不可见 |
| 会话 | 单连接恢复 | 跨连接、跨项目、跨 Flow | Namespace 与清理范围准确 |
回归记录不要只写"请求返回 403"。应写明拒绝发生在哪一层、有没有进入副作用函数、有没有生成子进程、是否发送网络包、是否创建 Session,以及用户看到的错误是否与内部终态一致。
如果安全策略只留下一个 error,后续很难判断是身份、网络还是进程门禁引发回归。建议给每一面稳定错误码,并在指标中分别计数:caller_identity_denied、secret_isolation_denied、code_policy_denied、process_policy_denied、network_policy_denied 和 session_isolation_denied。
十一、升级与回滚:版本标签不是验收报告
仍在 Langflow v1.11 线的环境,最低安全基线应提高到 v1.11.5;新环境可以固定 v1.12.0 或经验证的后续版本。但这只是升级起点,不是完成证明。
建议按以下顺序上线:
- 盘点所有公开 API、A2A、MCP、恢复和导入入口;
- 盘点 Flow 中的代码组件、Secret、stdio、Provider 自定义 URL 与共享 Session;
- 在隔离环境固定镜像或提交,运行上面的最小回归矩阵;
- 先灰度无代码、无持久 Secret、无 stdio 的公开 Flow;
- 观察六类拒绝码、匿名成功率、Provider 连接错误和 Session 命中变化;
- 再按显式业务需要逐项开放,而不是一次恢复全部旧能力。
回滚也要保住安全边界。如果新版本造成兼容问题,优先关闭公开入口、自定义 Provider 或 stdio 能力,回到受限运行模式;不要为了恢复业务把匿名主体重新映射为 Owner,也不要取消 DNS 连接固定。安全修复回滚到旧版本时,外层网关、出口 ACL 和组件禁用策略必须继续兜底。
十二、验证边界与可执行清单
本文的官方事实来自 Langflow v1.11.5、v1.12.0 Release、已核验安全 Patch 范围和 Langflow 官方仓库。本文没有声明新的 CVE,也没有把历史漏洞编号套到这批修复上。
本地验证只运行了框架无关的 Node.js 纯函数测试,环境为 v22.19.0,7 项通过、0 项失败。没有安装 Langflow,没有启动 MCP Server,没有访问 Provider,没有执行 DNS 重绑定攻击,也没有验证容器、数据库和生产代理配置。
发布或升级前,可以用下面的清单收口:
- 公开入口创建独立 Caller,不继承 Flow Owner;
- Owner、Caller 和 Tool Principal 在 Trace 中可区分;
- Secret 按组件元数据清理,运行、导出和日志均不泄漏;
- 公开 Flow 在解释器之前拒绝代码组件与生成代码逃逸;
- stdio 在真实连接 /
spawn前重验当前管理员策略; - Provider URL 校验全部 DNS 结果,并把结果固定到真实连接;
- 每次重定向重新检查,默认拒绝私网、回环和云元数据地址;
- Session Namespace 至少绑定连接、项目与 Flow;
- 首次运行、恢复、缓存命中和导入配置都经过最终执行点复验;
- 六类拒绝码能证明副作用未开始;
- 升级固定版本或镜像,回滚不撤掉外层安全兜底;
- 未验证的生产拓扑、代理和存储边界已明确记录。
一句话收尾:把 Flow 设为公开,只能打开一扇门;门后每一种能力仍要单独验票,而且验票位置必须贴着真实执行。