工作流都公开了,为什么还不能继承 Owner 权限?

0 阅读19分钟

把一个 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 所属关系
秘密隔离公开入口能否读持久 SecretSecret 注入、导出与日志泄漏清理后的组件值、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。任何一面通过,都不能替另一面签字。

三、第一步:把入口身份和资源所有权分开

最小动作不是给匿名用户补一个名字,而是建立不可歧义的主体模型。至少区分:

  1. owner:Flow、组件和持久资源的拥有者;
  2. caller:发起当前请求的登录或匿名主体;
  3. tool:实际执行某个工具或连接的服务身份。

运行上下文不要只保留 userId。同名字段很容易在恢复、队列投递和嵌套工具调用中被覆盖。可以显式保存:

{
  "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_keytoken 就删,看到 password 或普通命名的连接字符串却可能遗漏。Langflow 新修复采用组件元数据驱动的 Secret Scrubber,这个方向比字符串猜测可靠,因为“是否敏感”来自字段定义,不来自字段名长得像不像密钥。

建议把公开执行准备拆成一个纯函数:输入是持久化 Flow,输出是公开运行副本与拒绝原因。这个函数至少完成三件事:

  • 根据组件 Schema / 元数据清理所有 Secret 字段;
  • 发现自定义代码、生成代码或管理员限定组件时失败关闭;
  • 保留原始 Flow 不变,避免一次匿名调用反向污染 Owner 的编辑态。
持久化 Flow
→ 深拷贝为运行副本
→ 按组件元数据清理 Secret
→ 扫描代码与管理员限定组件
→ 生成公开执行计划或结构化拒绝

只在导出文件时做清理还不够。运行时、调试输出、Trace、异常消息和重试队列都可能拿到组件值。清理应发生在公开执行上下文形成之前;观测侧还要再做一次最小采集与脱敏,避免安全门禁自己成为秘密的第二个落点。

**过关证据:**使用名称分别为 api_keypasswordcredential 和普通业务名的四类 Secret 字段导出并运行公开 Flow,所有敏感值都不可见;代码组件返回稳定的策略拒绝,而不是进入 Python / JavaScript 解释器后才报错。

**回滚条件:**如果升级后合法公开 Flow 因组件元数据缺失被拒绝,不要把清理策略改回字段名匹配。先冻结公开入口,补齐组件元数据和迁移脚本,再灰度恢复。

五、第三步:注册时通过,不代表 stdio 连接时仍可执行

MCP stdio 配置会启动本地进程。它与普通 HTTP 地址不是同一种风险:命令、参数、工作目录、环境变量和继承权限都会变成宿主机副作用。

常见遗漏是只在“新增 MCP Server”或“导入配置”时检查管理员策略。配置随后进入数据库或 Bundle;真正连接时,运行时把它当作已经批准的对象直接使用。如果策略、角色、配置内容或宿主环境已经变化,旧检查就失去意义。

正确的检查点靠近 spawn

读取已保存配置
→ 规范化 command / args / cwd / env
→ 重新加载当前管理员策略
→ 绑定当前 Caller 和 Flow
→ 允许后才创建 stdio 进程

这里的“重新检查”不是重复做无意义工作。保存时检查保护配置仓库,连接前检查保护真实执行。两者面对的时间、主体和副作用不同。

连接前记录的审计信息也要克制:可以记录命令基名、参数数量、策略版本和拒绝码,不要把完整环境变量或可能含 Token 的参数原样写入日志。

**过关证据:**导入一个当时允许、随后被管理员禁用的 stdio 配置;再次连接必须在进程创建前失败,并确认没有子进程 PID、没有网络请求、没有工具列表返回。

**适用边界:**该门禁不替代容器、系统账户、沙箱与文件权限。它只保证策略拒绝发生在 spawn 之前;即使允许,也应继续限制进程实际能力。

六、第四步:Provider URL 要校验真实连接,不只校验字符串

自定义 Provider Base URL 通常同时携带 API Key。一次 SSRF 不只意味着“服务器可以访问内网”,还可能让凭据被发往攻击者控制的地址。

字符串层校验只能拦住明显的 127.0.0.1localhost 或私网字面量。域名可以先解析到公网地址,通过校验后再解析到回环或私网,这就是 DNS rebinding 的典型窗口。重定向也会把最初的公开地址带到内部目标。

Langflow v1.11.5 在 Anthropic 和 vLLM Multivector 路径中引入重校验、DNS 结果固定的 Client / Request Helper,并要求显式 allowlist 才能访问 loopback。工程上可以把网络门禁拆成五步:

  1. 规范化 scheme、hostname、端口和 URL Authority;
  2. 拒绝 userinfo、歧义 Authority 与不允许的协议;
  3. 解析全部 A / AAAA 结果,拒绝私网、回环、链路本地和云元数据地址;
  4. 把校验结果固定到实际连接,避免连接阶段重新解析到另一地址;
  5. 对每次重定向重新执行同样检查。

显式 allowlist 应表达“这个确切目标是业务需要”,而不是“所有 loopback 都可以”。本地开发可以允许指定主机和端口,生产默认仍应拒绝回环。代理、Service Mesh 和自定义 DNS 会改变真实连接路径,必须纳入回归。

**过关证据:**测试字面量回环、IPv6 回环、十进制 / 十六进制变体、首次公网后回环的双解析、公开地址到私网的 30x 跳转,以及合法 Provider。日志能回读最终固定 IP 和拒绝阶段,但不记录 Authorization Header。

**回滚条件:**如果 DNS 固定与现有代理或连接池不兼容,先关闭自定义 Provider 出口或限定已知域名,不要临时恢复“只校验 hostname”。

七、第五步:Session Namespace 必须包含连接、项目和 Flow

公开入口经常为了性能复用 Session、缓存或对话历史。如果 Namespace 只使用 flowId,同一个公开 Flow 的匿名访问者可能共享上下文;如果使用 Owner ID,匿名请求又可能命中拥有者的历史状态。

一个可检查的最小 Namespace 可以是:

projectId:flowId:connectionId

有租户和调用者时可以继续扩展,但不要用浏览器可随意伪造的显示名。连接断开、身份升级、Flow 版本变化和策略变化时,是否沿用旧 Session 也要有明确规则。

Session 隔离不只影响聊天记录。工具批准、缓存的 Provider Client、临时文件引用和恢复检查点都可能附着其上。把所有状态放进同一个不分主体的全局缓存,即使前面五面都正确,仍可能通过历史对象绕回旧权限。

**过关证据:**两个匿名连接调用同一个 Flow,Namespace 不同;同一连接恢复时 Namespace 稳定;切换项目或 Flow 后必然改变;清理一条连接不会删除其他连接的状态。

**适用边界:**Namespace 只提供逻辑隔离。跨租户强边界还应由数据库行级策略、独立存储前缀、加密密钥和资源配额共同保证。

八、本地 7 项实验:拒绝必须发生在副作用之前

为了验证六面门禁的组合方式,本次用 Node.js 标准库写了一个框架无关的纯函数。它不连接 Langflow、不读取 API Key、不访问网络,也不启动真实进程;目的只是检查失败分类与“先拒绝、后副作用”的顺序。

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,输出如下:

{"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。策略应对规范化后的最终 commandargscwd 和允许的环境变量集合签字。只比较 Server 名称,等于批准了一个可变别名。

第四种是Provider 域名通过校验,HTTP Client 又重新解析 DNS。校验阶段得到公网 IP,连接阶段如果让系统解析器再次查询,校验与使用之间仍有竞态。需要把已验证结果固定到实际连接,并处理 TLS SNI、Host Header、连接池复用和重定向;否则日志里虽然有“SSRF check passed”,Socket 仍可能连到另一地址。

第五种是Session Key 隔离正确,但缓存值里保留了旧权限对象。Namespace 不冲突只能保证键不同,不能保证值安全。Provider Client、工具列表、批准票据和临时文件句柄如果在策略升级后继续复用,新的调用仍可能带着旧能力。缓存项至少绑定主体、策略版本、Flow 版本和失效时间;安全策略提高时,应主动失效旧缓存,而不是等待自然过期。

这五个现场有同一条验收语句:检查通过后到副作用发生前,任何可影响授权的数据都不能再次变化;如果会变化,就在最后一个变化点之后重新检查。

十、不要只测成功路径:最小回归矩阵这样排

升级到 v1.11.5v1.12.0 后,只打开画布跑一次成功请求,覆盖不到这次变化的核心。至少交叉测试以下维度:

维度正常样本拒绝样本必查证据
主体登录 Caller匿名误继承 OwnerCaller / Owner / Tool 三种身份分离
入口私有 HTTP公开 MCP、恢复入口每个入口都重新准备安全上下文
组件普通模型节点代码组件、生成代码逃逸解释器或动态导入前拒绝
传输HTTP MCP被策略禁用的 stdio无子进程、无工具列表
网络固定公网 Providerloopback、私网、DNS 重绑定、重定向实际连接 IP 与每跳检查
秘密公开参数任意命名的 Secret 字段运行、导出、日志三处都不可见
会话单连接恢复跨连接、跨项目、跨 FlowNamespace 与清理范围准确

回归记录不要只写“请求返回 403”。应写明拒绝发生在哪一层、有没有进入副作用函数、有没有生成子进程、是否发送网络包、是否创建 Session,以及用户看到的错误是否与内部终态一致。

如果安全策略只留下一个 error,后续很难判断是身份、网络还是进程门禁引发回归。建议给每一面稳定错误码,并在指标中分别计数:caller_identity_deniedsecret_isolation_deniedcode_policy_deniedprocess_policy_deniednetwork_policy_deniedsession_isolation_denied

十一、升级与回滚:版本标签不是验收报告

仍在 Langflow v1.11 线的环境,最低安全基线应提高到 v1.11.5;新环境可以固定 v1.12.0 或经验证的后续版本。但这只是升级起点,不是完成证明。

建议按以下顺序上线:

  1. 盘点所有公开 API、A2A、MCP、恢复和导入入口;
  2. 盘点 Flow 中的代码组件、Secret、stdio、Provider 自定义 URL 与共享 Session;
  3. 在隔离环境固定镜像或提交,运行上面的最小回归矩阵;
  4. 先灰度无代码、无持久 Secret、无 stdio 的公开 Flow;
  5. 观察六类拒绝码、匿名成功率、Provider 连接错误和 Session 命中变化;
  6. 再按显式业务需要逐项开放,而不是一次恢复全部旧能力。

回滚也要保住安全边界。如果新版本造成兼容问题,优先关闭公开入口、自定义 Provider 或 stdio 能力,回到受限运行模式;不要为了恢复业务把匿名主体重新映射为 Owner,也不要取消 DNS 连接固定。安全修复回滚到旧版本时,外层网关、出口 ACL 和组件禁用策略必须继续兜底。

十二、验证边界与可执行清单

本文的官方事实来自 Langflow v1.11.5v1.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 设为公开,只能打开一扇门;门后每一种能力仍要单独验票,而且验票位置必须贴着真实执行。

官方来源