接口鉴权通过,对象却属于别人:LimeSurvey 跨问卷授权缺陷解析
背景与时间线
报告方 Fluid Attacks记录:2026 年 9 月 23 日 发现,24 日联系厂商,28 日确认、修复并公开。GitHub CVE 记录于 9 月 29 日 收录 CVE-2026-97685 ,该条目标明 Unreviewed,不能当作 GitHub 已审核结论。
证据来自报告方披露、公开 CVE 描述及项目修复提交。研究者验证版本为 Community Edition 7.3.0。本次没有核清最低修复发布版本,不能凭补丁已合并推断某个发行包必然安全。
技术原理:授权对象和写入对象必须是同一个
已确认链路是:接口针对 URL 中的问卷检查权限,但后续根据请求中的全局题目或答案 ID 查找对象,没有验证对象属于被授权问卷。具备创建问卷权限的已认证用户,可能借自己的合法问卷上下文修改其他问卷对象。
**工程分析:**问题不在于权限检查完全不存在,而在于权限检查的主语变了。代码读起来可能每一步都合理:先检查问卷,再查询题目,再执行更新。但这三步之间缺少关系约束。
更稳健的数据访问方式是把授权范围写进查询条件,而不是先全局查到对象再期望每个调用者记住补查。对于批量操作,还要保证一个非法对象不会在前面合法对象已经写入后才被发现。
| 校验层 | 需要证明的关系 |
|---|---|
| 用户与问卷 | 用户可以编辑该问卷 |
| 问卷与题目 | 题目确实属于该问卷 |
| 题目与答案 | 答案属于预期题目和问卷 |
| 批量操作 | 所有目标通过校验后再提交 |
影响范围与证据边界
本次披露证明跨问卷完整性风险,必要条件包含认证及相应操作权限。本文不将其扩大成匿名接管、任意 SQL 注入或 RCE。报告页的利用演示代表受控验证,不是现实攻击活动证明。
修复提交可用于代码核对,但部署团队仍需核验其是否包含在自己的发行包、容器镜像或下游回补中。在确认前,可评估限制相关 REST 修改功能和缩小创建、编辑权限范围,作为临时风险降低措施。
无害内存模型
python
surveys = {10: {'owner': 'alice'}, 20: {'owner': 'bob'}}
questions = {101: {'survey': 10, 'text': 'A'},
201: {'survey': 20, 'text': 'B'}}
def update(user, survey_id, changes):
if surveys[survey_id]['owner'] != user:
raise PermissionError('survey denied')
# 全量校验在任何写入之前完成。
for qid, text in changes:
if qid not in questions or questions[qid]['survey'] != survey_id:
raise PermissionError('question outside scope')
for qid, text in changes:
questions[qid]['text'] = text
try:
update('alice', 10, [(101, 'new'), (201, 'wrong')])
except PermissionError:
pass
assert questions[101]['text'] == 'A'
assert questions[201]['text'] == 'B'
update('alice', 10, [(101, 'valid')])
assert questions[101]['text'] == 'valid'
print('ownership and atomicity model passed')
示例不连接 LimeSurvey,也不构造真实请求。真实数据库中还需要事务与并发控制,避免校验后对象归属发生变化;单线程字典模型不具备这些保证。
研发与安全团队行动清单
P0:确认部署包含修复
记录当前版本和制品标识,追踪官方修复进入发行版本的情况。评估相关接口可达性和低权限账号范围。审计日志可检查用户、URL 问卷 ID、实际修改对象归属是否一致,但不要把一次合法跨对象管理操作直接判成攻击。
P1:测试关系,而非只测试角色
准备两个测试用户、两份问卷、分别归属的题目和答案,覆盖同租户不同所有者、不同问卷、混合批量以及不存在的 ID。每个拒绝测试都应确认数据未发生部分写入。
P2:约束下沉与审计对账
将授权上下文显式传入服务层,避免持久化层只接受裸全局 ID。数据库查询将范围作为条件,更新结果为零时区分无权限和不存在的内部处理,同时控制对外错误信息,避免泄露对象存在性。
安全团队可从控制器到仓储层追踪同一请求内的标识符:哪个被检查,哪个被修改,中间是否发生重新解析。这个方法适用于订单、项目、文档和多租户配置系统,但不意味着它们天然存在同类漏洞。
总结
授权必须跟着最终对象走。一次成功的入口鉴权不能自动覆盖后来从请求体解析出的所有全局 ID。把归属约束写进查询和事务,比在每个接口补零散判断更容易维护。