CVE-2026-43948None!=None为False:一个让授权守卫静默失效的Python陷阱

admin 2026-05-14 11:17:31 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: CVE-2026-43948披露了wger健身管理系统中的高危授权绕过漏洞,因Python中None!=None返回False导致权限守卫静默失效。攻击者利用此漏洞可重置任意gym字段为None的用户密码并直接获取明文密码,实现账户接管。漏洞CVSS评分9.9,影响版本至wger2.6之前。修复建议包括替换比较逻辑为显式处理None值、采用白名单授权策略及避免响应返回敏感信息。 综合评分: 92 文章分类: 漏洞分析,WEB安全,代码审计,实战经验,安全开发


cover_image

CVE-2026-43948 None != None 为 False:一个让授权守卫静默失效的 Python 陷阱

原创

CVE-SEC CVE-SEC

CVE-SEC

2026年5月13日 09:30 新疆

在小说阅读器读本章

去阅读

None != None 为 False:一个让授权守卫静默失效的 Python 陷阱

引言

在安全代码审计中,最危险的漏洞往往不是复杂的逻辑错误,而是那些看起来”理所当然正确”的代码。CVE-2026-43948 正是这样一个案例——开发者使用了完全符合直觉的 Python 比较运算符,却在特定条件下制造了一个 CVSS 9.9 分的严重授权绕过漏洞,最终导致攻击者可以一步完成任意账户接管,且受害者原密码永久失效。

这个漏洞的核心,是一行在 Python 中求值为 False 的表达式:None != None


漏洞概述

CVE 编号: CVE-2026-43948

受影响产品: wger,一款基于 Python/Django 构建的开源健身与健身房管理系统,广泛用于健身房会员管理、训练计划制定、营养追踪等场景。

漏洞类型: 授权绕过(Authorization Bypass)导致账户接管(Account Takeover)

CVSS 评分: 9.9(Critical,CVSS 3.x)

修复版本: wger 2.6

披露日期: 2026-05-12

参考: https://github.com/wger-project/wger/security/advisories/GHSA-mhc8-p3jx-84mm

漏洞存在于 wger 的两个视图函数中:reset_user_password 和 gym_permissions_user_edit。这两个视图均依赖对 gym 字段的比较来实施授权控制,但由于使用了 != 运算符比较两个 None 值,导致授权守卫在特定条件下被静默绕过。


技术深度分析

Python 对象比较的语义陷阱

Python 中存在两种相等性比较机制:

  • == / !=:值相等性比较,调用对象的 __eq__ 方法
  • is / is not:身份比较,判断两个变量是否指向同一个对象

对于 None 这一单例对象,Python 保证整个解释器生命周期内只存在一个 None 实例。因此:

>>> None is None
True
>>> None == None
True
>>> None != None
False

上述三个表达式的结果均符合预期,None != None 确实求值为 False,这在数学逻辑上是正确的。

那么问题出在哪里?

问题不在于 Python 的行为本身,而在于开发者将这个比较结果用于授权守卫时,对”False”的语义理解出现了偏差。

授权逻辑缺陷的本质

wger 的健身房管理模型中,用户和健身房之间存在关联关系。gym 字段用于标识用户所属的健身房。当用户不属于任何健身房时,该字段值为 None

授权逻辑的设计意图是:持有 gym.manage_gym 权限的管理员只能操作与自己同属一个健身房的用户。用伪代码表示,预期逻辑如下:

# 预期:攻击者的 gym 与目标用户的 gym 不同时,拒绝操作
if attacker.gym != target.gym:
    raise PermissionDenied

这段逻辑在正常情况下工作良好。当攻击者属于 gym_A,目标用户属于 gym_B 时,gym_A != gym_B 为 True,权限检查触发,操作被拒绝。

然而,当两者的 gym 字段均为 None 时:

attacker.gym = None
target.gym   = None

None != None  # 求值为 False

条件为 False,权限检查不触发,操作被放行。

这意味着:任何持有 gym.manage_gym 权限且 gym=None 的攻击者,可以对所有 gym=None 的用户执行受保护操作,而授权守卫对此毫无察觉。

为何这是一个”静默”绕过

“静默”二字至关重要。授权守卫没有抛出异常,没有记录警告,没有返回错误响应——它只是安静地判断”条件不满足,无需拦截”,然后让请求继续执行。从日志和监控的角度看,这与一次合法的授权操作没有任何区别。

这类静默失效(silent failure)是授权漏洞中最难被发现的一类,因为它不会在测试中产生明显的异常行为,只有在特定的边界条件(gym=None)下才会显现。


漏洞代码逻辑还原

基于已公开的漏洞信息,受影响的视图函数授权逻辑可还原如下(以 reset_user_password 为例):

# 存在漏洞的代码逻辑(还原)
def reset_user_password(request, user_pk):
    target_user = get_object_or_404(User, pk=user_pk)

    # 授权检查:确保操作者与目标用户属于同一健身房
    if request.user.userconfig.gym != target_user.userconfig.gym:
        raise PermissionDenied

    # 生成新密码并重置
    new_password = generate_random_password()
    target_user.set_password(new_password)
    target_user.save()

    # 将新明文密码返回至 HTML 响应体
    return render(request, 'password_reset_confirm.html', {
        'new_password': new_password
    })

当 request.user.userconfig.gym 和 target_user.userconfig.gym 均为 None 时,第 5 行的条件求值为 FalsePermissionDenied 不被触发,函数继续执行密码重置操作。

尤为严重的是最后一步:新生成的明文密码被直接渲染至 HTML 响应体并返回给攻击者。这意味着攻击者在单次 HTTP 请求中即可完成:

  1. 绕过授权检查
  2. 强制重置目标用户密码
  3. 获取新明文密码
  4. 使用新密码登录目标账户

受害者的原密码在此过程中被永久覆盖,账户被完全接管,且受害者在未收到任何通知的情况下即被锁定在自己的账户之外。

gym_permissions_user_edit 视图存在类似的授权逻辑缺陷,允许攻击者修改目标用户的权限配置,进一步扩大攻击面。


攻击场景与影响

攻击前提条件

攻击者需满足以下条件:

  1. 在目标 wger 实例上拥有一个合法账户
  2. 该账户持有 gym.manage_gym 权限
  3. 该账户的 gym 字段为 None(即不属于任何具体健身房)

在 wger 的权限模型中,gym.manage_gym 是一个相对常见的管理权限,通常授予健身房管理员。在多租户部署场景下,存在 gym=None 的管理账户并非罕见情况(推测:可能用于平台级管理员或初始化阶段的账户)。

攻击步骤

  1. 攻击者使用持有 gym.manage_gym 且 gym=None 的账户登录
  2. 枚举或猜测目标用户的 user_pk(Django 默认使用自增整数主键,可预测)
  3. 向 reset_user_password 视图发送包含目标 user_pk 的请求
  4. 从 HTML 响应体中提取新明文密码
  5. 使用目标用户的用户名和新密码登录,完成账户接管

整个攻击过程无需任何漏洞利用工具,仅需标准 HTTP 请求即可完成。

影响范围

  • 直接影响: 所有 gym=None 的用户账户均可被接管,包括其他管理员账户
  • 数据泄露: 明文密码在响应体中暴露,若存在中间人或日志记录,泄露范围进一步扩大
  • 持久影响: 受害者原密码被永久覆盖,即使攻击被发现,受害者也需通过其他渠道恢复账户访问
  • 权限提升: 通过接管高权限账户,攻击者可进一步获取平台管理权限

检测与应急响应

检测方法

日志审计: 检查 wger 应用日志中 reset_user_password 和 gym_permissions_user_edit 端点的访问记录,重点关注:

  • 短时间内对多个不同 user_pk 的重置请求
  • 来自同一账户的批量操作
  • 非工作时间的密码重置操作

数据库审计: 检查用户表中密码哈希的变更记录(如启用了数据库审计日志),识别非用户本人发起的密码变更。

账户异常: 排查近期出现登录失败后立即成功登录的账户,可能指示密码被重置后攻击者登录。

应急响应

  1. 立即升级: 将 wger 升级至 2.6 版本,这是消除漏洞的根本措施
  2. 权限审查: 审查所有持有 gym.manage_gym 权限且 gym=None 的账户,评估是否存在可疑账户
  3. 强制重置: 对所有 gym=None 的用户账户强制要求重新设置密码,以应对可能已发生的账户接管
  4. 会话失效: 使所有活跃会话失效,强制重新认证
  5. 日志留存: 保留完整的访问日志用于后续取证分析

修复建议

官方修复方向

正确的修复方式是将 != 运算符替换为能够正确处理 None 值的比较逻辑。在 Python/Django 中,推荐的做法是:

# 修复后的授权检查
if request.user.userconfig.gym_id != target_user.userconfig.gym_id:
    raise PermissionDenied

使用数据库层面的外键 ID(整数或 None)进行比较,而非 ORM 对象实例。对于两个均为 None 的 gym_idNone != None 仍为 False,但此时需要额外的逻辑明确处理这一边界情况:

# 更严格的修复方式
attacker_gym_id = request.user.userconfig.gym_id
target_gym_id   = target_user.userconfig.gym_id

# 明确拒绝 gym=None 的跨用户操作,或要求 gym 必须匹配
if attacker_gym_id is None or attacker_gym_id != target_gym_id:
    raise PermissionDenied

或者采用白名单思路,明确定义”允许操作”的条件,而非”拒绝操作”的条件:

# 白名单思路:仅当 gym 非空且匹配时允许
if not (attacker_gym_id is not None and attacker_gym_id == target_gym_id):
    raise PermissionDenied

明文密码响应的修复

除授权逻辑外,将明文密码返回至 HTML 响应体本身也是一个独立的安全问题。正确的做法是:

  • 密码重置后仅返回操作成功的状态,不返回密码本身
  • 通过已验证的邮件或其他带外渠道通知用户新密码,或引导用户自行设置新密码

总结:Python 对象比较的安全编码思考

CVE-2026-43948 给安全开发者提供了一个值得深思的案例。

None != None 求值为 False 在 Python 语言规范中是完全正确的行为,没有任何 bug。问题在于开发者在设计授权逻辑时,没有将 None 作为一个需要特殊处理的边界值加以考虑。

在安全敏感的比较场景中,以下原则值得遵循:

原则一:对 None 保持警惕。 任何用于授权判断的字段,都应明确定义 None 值的语义,并在代码中显式处理这一情况,而非依赖默认的比较行为。

原则二:使用 is/is not 比较 None。 Python 官方风格指南(PEP 8)明确建议使用 is None 和 is not None 来检查 None 值,而非 == None 或 != None。这不仅是风格问题,在某些自定义 __eq__ 的对象上,== None 可能产生意外结果。

原则三:授权逻辑优先使用白名单。 基于”拒绝”条件的授权逻辑(黑名单思路)容易在边界条件下产生静默绕过。基于”允许”条件的白名单思路更为安全,因为任何未被明确允许的情况都会被默认拒绝。

原则四:敏感操作的响应不应包含秘密信息。 即使授权逻辑正确,将明文密码返回至响应体也是独立的安全风险,应在设计阶段即予以排除。

原则五:边界值测试是安全测试的必要组成部分。 对于涉及授权的代码,测试用例应覆盖所有字段为 None、空字符串、零值等边界情况,而非仅测试正常业务流程。

一个字符的差异——!= 与 is not——在特定条件下造就了 CVSS 9.9 分的严重漏洞。这提醒我们,安全编码的细节决定成败,对语言特性的深入理解是写出安全代码的前提。


漏洞信息来源: https://github.com/wger-project/wger/security/advisories/GHSA-mhc8-p3jx-84mm

披露日期: 2026-05-12

修复版本: wger 2.6


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:CVE-SEC CVE-SEC CVE-SEC《CVE-2026-43948 None != None 为 False:一个让授权守卫静默失效的 Python 陷阱》

评论:0   参与:  0