/api/v1/responses IDOR 漏洞此存储库包含一个本地 Docker 实验室,用于重现和验证 CVE-2026-55255,这是一个影响 Langflow 的 OpenAI 兼容 Responses API 的不安全直接对象引用 (IDOR) 漏洞。
Langflow 是一个用于构建和部署人工智能驱动的代理和工作流的开源平台。该漏洞行为影响 /api/v1/responses 端点,在该端点上,经过身份验证的攻击者可以提供另一个用户的流 UUID 作为 model 值,从而导致 Langflow 执行该受害者拥有的流。
此实验室比较两个 Langflow 版本:
| Service | Langflow 版本 | 目的 | URL |
|---|---|---|---|
| vuln | 1.9.0 | 易受攻击的比较目标 | http://localhost:7860 |
| patched | 1.9.1 | 已修补的比较目标 | http://localhost:7861 |
此本地实验室中演示的 HTTP 验证路径为:```text Authenticated attacker API key → POST /api/v1/responses → request body sets model to victim-owned flow UUID → vulnerable target executes the victim-owned flow → patched target returns flow_not_found and does not execute the victim-owned flow
在存在漏洞的目标中,攻击者拥有的API密钥可以执行受害者拥有的流程,并且响应中包含仅受害者可见的标记:```text
VICTIM_ONLY_CONTEXT_55255_VULN
在已修补的目标中,相同的跨用户请求不会返回受害者标记,而是返回一个OpenAI风格的错误主体:```json {"error":{"message":"Flow with id '' not found","type":"invalid_request_error","code":"flow_not_found"}}
本实验使用 Langflow 1.9.0 和 Langflow 1.9.1 验证了存在漏洞与已修复版本的 HTTP 行为。
该实验范围仅限于本地 Docker 服务。它不针对外部系统,也不包含凭证窃取、数据库转储、破坏性负载、外部回调、恶意软件、持久化或后利用活动。
## 已验证的事实
| 声明 | 证据 | 如何在本实验中验证 |
| ---- | ---- | ------------------- |
| CVE-2026-55255 影响 Langflow 的 `/api/v1/responses` 端点。 | GitHub 安全公告 GHSA-qrpv-q767-xqq2 描述了 `/api/v1/responses` 中的 IDOR 漏洞。 | 查看参考资料部分,并在两个本地目标上运行 PoC。 |
| GitHub 安全公告列出的受影响版本为 `< 1.9.1`,修复版本为 `1.9.1`。 | GitHub 安全公告 GHSA-qrpv-q767-xqq2。 | 比较 `docker-compose.yml` 中存在漏洞和已修复的目标版本。 |
| 一些下游漏洞来源对确切修复版本存在不一致意见。 | GitHub/GitLab 将 `1.9.1` 列为已修复;一些下游情报页面提到 `1.9.2` 或包含混合措辞。 | 查看参考资料部分,并依赖本实验对测试的 1.9.1 行为的验证。 |
| 本实验使用 Langflow 1.9.0 作为存在漏洞的比较目标。 | `vuln` 服务使用 `langflowai/langflow:1.9.0`。 | 检查 `docker-compose.yml` 并运行 `docker compose ps`。 |
| 本实验使用 Langflow 1.9.1 作为已修复的比较目标。 | `patched` 服务使用 `langflowai/langflow:1.9.1`。 | 检查 `docker-compose.yml` 并运行 `docker compose ps`。 |
| Langflow 的响应 API 使用 `POST /api/v1/responses`。 | Langflow 文档描述了兼容 OpenAI 的响应 API 端点。 | 针对 `/api/v1/responses` 运行 PoC 或手动 curl 请求。 |
| Langflow 的响应 API 接受流程 ID 作为 `model` 值。 | Langflow 文档指出 `model` 值会被替换为 `flow_id`。 | 检查 PoC 请求体。 |
| Langflow API 请求需要通过 `x-api-key` 提供 API 密钥。 | Langflow API 文档描述了使用 `x-api-key` 头进行 API 密钥身份验证。 | 检查 PoC 请求头。 |
| PoC 是基于请求的。 | `poc/validate_idor.py` 发送 HTTP 请求,不调用 Docker、Docker Compose、shell 命令或容器 API。 | 检查 `poc/validate_idor.py`。 |
| 存在漏洞的目标会执行一个受害者拥有的流程,使用攻击者拥有的 API 密钥。 | 存在漏洞的响应返回 `VICTIM_ONLY_CONTEXT_55255_VULN`。 | 使用受害者流程 ID 和攻击者 API 密钥运行存在漏洞的 PoC 命令。 |
| 已修复的目标会阻止相同的跨用户执行路径。 | 已修复的响应返回 `error.code = flow_not_found`,并且不返回受害者标记。 | 使用受害者流程 ID 和攻击者 API 密钥运行已修复的 PoC 命令。 |
## 假设与未知情况
本实验使用 Langflow 1.9.0 作为存在漏洞的比较目标,因为 GitHub 安全公告 GHSA-qrpv-q767-xqq2 将 1.9.1 之前的版本标记为受影响,且本地测试确认了 1.9.0 中的漏洞行为。
本实验使用 Langflow 1.9.1 作为已修复的比较目标,因为 GitHub 安全公告 GHSA-qrpv-q767-xqq2 将 1.9.1 列为已修复版本,且本地测试确认 1.9.1 通过 `flow_not_found` 阻止了测试中的跨用户 `/api/v1/responses` 执行路径。
来源之间存在版本差异。GitHub 和 GitLab 公告将 1.9.1 之前的版本列为受影响,1.9.1 列为已修复。一些下游漏洞情报页面提到 1.9.2 或包含关于修复版本的混合措辞。本仓库记录了这一差异,并直接验证了测试行为。```text
Langflow 1.9.0
→ attacker API key + victim flow UUID
→ victim marker returned
→ vulnerable behavior observed
Langflow 1.9.1
→ attacker API key + victim flow UUID
→ flow_not_found
→ victim marker not returned
→ blocked behavior observed
本实验假设攻击者已知道受害者的 flow UUID。该 PoC 不会暴力破解 flow ID、枚举 flows 或尝试发现受害者 flow ID。
本实验关注于可观察的 HTTP 行为:```text POST /api/v1/responses
使用此请求形状:```json
{
"model": "<victim-flow-id>",
"input": "cross-user CVE-2026-55255 validation request",
"stream": false
}
该实验室在存在漏洞的目标中演示了未经授权的跨用户流程执行,并在修补后的目标中演示了被阻止的行为。
该实验室未演示:
CVE-2026-55255的根本原因是Langflow的流程解析逻辑中存在授权漏洞。
/api/v1/responses端点通过model字段接受流程UUID。在存在漏洞的版本中,get_flow_by_id_or_endpoint_name()内的UUID查找路径可以直接通过主键加载Flow对象,而未强制检查解析后的Flow.user_id是否与已认证的API密钥用户匹配。
存在漏洞的行为可总结为:```text Attacker owns API key → attacker sends POST /api/v1/responses → model contains victim-owned flow UUID → flow resolver loads Flow by UUID → resolver does not enforce Flow.user_id == attacker_user.id → response endpoint executes the victim-owned flow → attacker receives victim flow output
修补后的行为可概括为:```text
Attacker owns API key
→ attacker sends POST /api/v1/responses
→ model contains victim-owned flow UUID
→ flow resolver loads candidate Flow
→ resolver compares Flow.user_id with authenticated API-key user id
→ cross-user lookup is treated as not found
→ response endpoint returns flow_not_found
→ victim-owned flow is not executed
安全问题不在于攻击者可以使用自己的流程调用 /api/v1/responses。这是预期行为。问题在于,当提供受害者的流程 UUID 时,一个低权限的已认证用户可以导致该端点执行另一个用户拥有的流程。
安全教训是:```text Object lookup by UUID is not authorization. Every object lookup used by an authenticated API route must be scoped to the authenticated principal or followed by a strict ownership check before the object is used.
## 源码分析
通过对比 Langflow `v1.9.0` 和 `v1.9.1`,确认了源码级别的问题。
用于审查的已检查源码标签如下:
| 版本 | Git 提交 |
| ------- | ---------- |
| v1.9.0 | `a47f2ad17eb662e940c550cfccb64a87dddd7e0b` |
| v1.9.1 | `dc26d19c1ed5b2779a3a759f78a747f47089c534` |
相关辅助函数为:```python
async def get_flow_by_id_or_endpoint_name(flow_id_or_name: str, user_id: str | UUID | None = None) -> FlowRead:
在易受攻击的 UUID 分支中,流程通过 ID 加载:```python flow_id = UUID(flow_id_or_name) flow = await session.get(Flow, flow_id)
缺失的安全相关检查是:```python
flow.user_id == authenticated_user.id
如果没有那个所有者检查,一个有效的 Flow UUID 就足以解析出一个 Flow 对象,即使它属于另一个用户。
修补后的版本添加了对 user_id 的规范化,并在 UUID 路径上强制执行所有者作用域:```python
if flow is not None and uuid_user_id is not None and flow.user_id != uuid_user_id:
flow = None
重要的行为是:```text
if the flow exists
and the flow belongs to another user
then treat it as not found
这就是为什么修补后的实验室响应返回:```json {"error":{"code":"flow_not_found"}}
而不是执行受害者拥有的流程。
对于此实验,主要易受攻击的行为是 `/api/v1/responses` 执行路径到达 `get_flow_by_id_or_endpoint_name()` 并解析受害者拥有的流程 UUID 而不强制执行所有权。
该补丁还加固了相关的流程执行路由。在 `endpoints.py` 中,之前使用原始助手作为 FastAPI 依赖项的路由已从以下内容更改:```python
flow: Annotated[FlowRead, Depends(get_flow_by_id_or_endpoint_name)]
到经过认证的包装器,例如:```python async def get_flow_for_api_key_user( flow_id_or_name: str, api_key_user: Annotated[UserRead, Depends(api_key_security)], ) -> FlowRead: return await get_flow_by_id_or_endpoint_name(flow_id_or_name, api_key_user.id)
这些包装器更改是针对其他流程执行路由(如 `/api/v1/run*`)的相关加固。它们确保辅助函数接收到经过身份验证的用户ID,而不是依赖于普通的请求参数。本实验所展示的核心修复仍然是 `get_flow_by_id_or_endpoint_name()` 内部的解析器侧所有权检查。
因此,源码级别的修复有两个相关部分:```text
Core resolver fix:
enforce owner scoping before returning a Flow object
Related route dependency hardening:
pass the authenticated API-key or session user's ID into the resolver
Langflow 1.9.1 通过将跨用户查找视为未找到,并确保相关流程执行路径将已验证的用户上下文传递给解析器,从而加强了易受攻击的流程解析路径。
核心补丁逻辑为:```python if flow is not None and uuid_user_id is not None and flow.user_id != uuid_user_id: flow = None
该补丁还为需要解析流程的路由添加了经过身份验证的包装器依赖项:```python
async def get_flow_for_api_key_user(...):
return await get_flow_by_id_or_endpoint_name(flow_id_or_name, api_key_user.id)
async def get_flow_for_current_user(...):
return await get_flow_by_id_or_endpoint_name(flow_id_or_name, current_user.id)
与安全相关的变更是:```text Before: flow UUID → session.get(Flow, flow_id) → Flow object returned without owner scoping → downstream execution path can run victim-owned flow
After: flow UUID → session.get(Flow, flow_id) → compare Flow.user_id with authenticated user id → cross-user result becomes None → shared not-found behavior fires → victim-owned flow is not executed
补丁还减少了信息泄露。跨用户访问被视为“未找到”,而不是返回一个不同的授权响应,该响应可能会揭示另一个用户的流程是否存在。
这个实验将源代码审查和运行时验证分开:```text
Source patch review:
explains why the vulnerable resolver could return a victim-owned flow.