
查找携带比同级路由更弱的授权控制的 API 路由。
它仅凭源码就在 Open WebUI 中发现了 CVE-2026-45316,无需任何安全公告信息:
[MISMATCH] POST /notes/{id}/pin (notes.py:pin_note_by_id)
3/3 comparable write operations on note require has_access(write);
this route requires only has_access(read)
POST /notes/{id}/update has_access(write)
POST /notes/{id}/access/update has_access(write)
DELETE /notes/{id}/delete has_access(write)
在五个生产代码库的 2,568 条路由中,它产生了 4 个发现。其中两个是真的。本 README 解释这两个数字。
一大类授权漏洞并非源于检查逻辑被破坏,而是源于某条路由上的检查缺失或被削弱,而其同级路由都做对了。
Portainer 对四个同级模板端点做了授权,却没有对第五个做。Signal K 对 HTTP 登录做了速率限制,却没有对 WebSocket 登录做。Open WebUI 的笔记置顶路由在检查读权限的同时修改了笔记,而所有其他笔记修改操作都检查写权限。
总是同样的形态:
✓ ✓ ✓ ✓ ✗
规则本就存在于代码库中。某条路由破坏了它。因此,从代码中重建规则并报告例外即可。无需策略文件,无需配置,无需注解。证明集就是同级路由本身。
python3 enforcement_coverage.py /path/to/repo
python3 enforcement_coverage.py /path/to/repo --density
python3 enforcement_coverage.py /path/to/repo --json
需要 Python 3.10+,无依赖。仅适用于 FastAPI。
判定结果为 MISSING、MISMATCH、PRESERVED、UNKNOWN。UNKNOWN 表示弃权,永不报告。
提取。 从签名 Depends()、装饰器 dependencies=[]、路由器级依赖、函数体、包装函数和权限类列表中解析出控制逻辑。
函数体提取至关重要。在 Open WebUI 中,608 个处理器里有 216 个在函数内部携带关键检查:
if user.role != 'admin' and not await AccessGrants.has_access(
user_id=user.id, resource_type='note',
resource_id=note.id, permission='write', db=db,
):
raise HTTPException(status_code=403)
操作类别取决于处理器对资源执行了什么操作,而非 HTTP 动词。POST /notes/{id}/chat 是一个读取笔记的 POST。
词汇分类。 同一控制族中的两个值不一定构成强度层级:
has_access {read, write} LEVEL
ensure_flow_permission {create,delete,execute,read,write} ACTION
has_permission {features.notes, workspace.tools} SCOPE
只有 LEVEL 词汇支持强度比较。在修复该问题之前,将 FlowAction.CREATE 与占主导的 WRITE 进行比较,在 Langflow 中产生了六个误报。
方向过滤。 只报告向更宽松控制方向偏离的情况。比先例更严格不构成漏洞。
| 仓库 | 路由 | 受控 | 发现 |
|---|---|---|---|
| LiteLLM | 808 | 87.1% | 0 |
| Danswer / Onyx | 653 | 95.9% | 0 |
| Open WebUI | 529 | 97.2% | 2 |
| Netflix Dispatch | 291 | 36.1% | 1 |
| Langflow | 287 | 47.4% | 1 |
两个真正的发现。
Open WebUI 中的 POST /notes/{id}/pin —— CVE-2026-45316。
另一个发现是 Netflix Dispatch 中的权限不对称:POST /{incident_id}/resources 使用 IncidentViewPermission,而六个同级写操作使用 IncidentEditPermission。IncidentViewPermission 对任何非受限事件都返回 True;IncidentEditPermission 则要求管理员、指挥员或报告人。该处理器将工单和群组创建任务加入队列,没有进一步的检查。
未报告,因为没有可报告的地方:该仓库已于 2025 年 9 月 3 日被 Netflix 归档且为只读;它没有 SECURITY.md;归档仓库的私有漏洞报告功能已被禁用;而且 Dispatch 被明确列为 Netflix 漏洞赏金计划的范围之外。在这里发布是仅存的披露渠道。它属于低严重性——需要经过身份验证的组织成员,且只影响非受限事件——而且该项目已不再维护。
两个误报。 POST /tools/{id}/valves/user/update 写入的是用户自己的阀门设置,合理地只需要对工具的读权限——变更目标与授权主体是不同的实体。另一个是 Langflow 的知识库路由,其同级保护并非必需。
这是最有用的部分。
Danswer 的控制覆盖率为 95.9%,却产生了零发现,其中 80.7% 为 UNKNOWN。它的词汇是 require_permission('basic_access')、('manage_connectors')——一个能力命名空间,而非强度层级。你不能说 manage_connectors 弱于 read_connectors。
真正能预测发现的是带有序强度参数的资源级权限调用,例如 has_access(resource, read|write)。五个代码库中只有一个具备这一点。
| 仓库 | 惯用写法 |
|---|---|
| Open WebUI | AccessGrants.has_access(resource_type=, permission=) |
| Langflow | ensure_<resource>_permission(user, Action.X) |
| LiteLLM | 对 user_api_key_dict.user_role 的角色比较 |
| Netflix Dispatch | Depends(PermissionsDependency([CaseEditPermission])) |
| Danswer | require_permission('basic_access') |
五个仓库,五种惯用写法。每一种都需要先完成提取器相关工作,分析才能运行起来。
在任何仓库、任何配置下都是如此。大多数路由并不属于具有一致控制的、由三个或更多路由组成的同级路由族。
这些缺陷中没有一个是事先预料到的。
Depends()缺陷 13 最有趣。Dispatch 的标签推荐路由缺少其同级路由携带的 CaseViewPermission。但该权限对任何非受限案件都返回 True,而且服务已经以内联方式检查了 visibility == restricted。两条路径是等价的。要检测出这一点需要语义等价分析,而非结构分析。
该机制确实有效。它仅利用存在漏洞的提交合并之前就可用的信息,从源码中还原出了一个已公开的 CVE 和一个未报告的授权缺口,并带有可审计的证明集。
产出大约为每 1,000 条路由一个发现,而且它需要某种架构——五个大型代码库中只有一个具备这种架构。
对于具有资源级权限词汇的代码库,它是一款有用的审计工具。但就现有证据而言,它并非通用扫描器。
通过推断隐式假设来静态检测访问控制漏洞的做法可追溯至 USENIX Security 2011。ACMiner 挖掘了 Android 中间件中的授权检查。Semgrep 提供了针对缺失授权的 AI 驱动检测,并在一次客户评估中报告了 61% 的精确率。OWASP 发布了一份授权回归测试速查表,其推荐的方法是手动维护一个 Actor × Resource × Action 矩阵。
这是一种范围狭窄、确定性的思路:只还原同级路由能够证明的内容,其余一律弃权。
采用 MIT 许可证。