[Security] CRITICAL auth bypass (default admin superuser for unauthenticated requests) + JWT type confusion + password change doesn't revoke tokens
R2R — 认证绕过/提权(默认 admin superuser)+ JWT Token 类型混淆 + 改密不撤销 token
本报告记录了在 main 分支(审计时最新)发现的多个高危/严重漏洞,均已对照当前源码核实。最严重的是 CRITICAL 级认证绕过:auth_wrapper 在未认证请求时返回默认 admin 用户(superuser)。
注:R2R 的 SECURITY.md 明确允许用 issue 报告漏洞("Vulnerability Disclosure" 模板,可设 confidential)。本报告按其流程提交,建议维护者标记为 confidential。
漏洞 1 — [CRITICAL] 认证绕过/提权(默认 admin superuser)
位置: py/core/base/providers/auth.py:122-138 (auth_wrapper)
def auth_wrapper(self, public: bool = False, ...):
async def _auth_wrapper(...):
if (...) and ((not self.config.require_authentication) or public): # L134
return await self._get_default_admin_user() # L138 返回 superuser_get_default_admin_user 返回的 admin 用户在 provider 初始化时设为 is_superuser=True。这意味着:
- 默认配置(
require_authentication=False,L29):每个未认证请求以 admin superuser 执行——跨整个 API 的完整读写删除权限。 - 即使
require_authentication=True:任何auth_wrapper(public=True)注册的端点对未认证调用者授予完整 superuser 权限。public 端点应返回受限/匿名身份,绝不应是 superuser admin。
影响:
- 默认配置:攻击者无凭证发任意请求(如
POST /v3/users、DELETE /v3/documents),以 admin superuser 执行,完整读写删除所有资源。 - public 端点:即使生产开了认证,任何 public 端点(如 search、RAG)对未认证请求返回 superuser,攻击者省略凭证即获 superuser 级访问,且下游任何
user.is_superuser检查都通过。
复现:
# 默认配置(require_authentication=False):
curl -X POST "http://<host>/v3/users" \
-H "Content-Type: application/json" -d '{"email":"[email protected]",...}'
# 无 Authorization,auth_wrapper 返回默认 admin superuser,请求以 superuser 执行。
# public 端点(即使 require_authentication=True):
curl "http://<host>/v3/search?q=test" # 假设 search 标记 public=True
# auth_wrapper 返回 superuser。修复: public/未认证请求返回受限匿名身份(如 is_superuser=False 的 anonymous user),绝不返回 admin superuser。require_authentication=False 应只用于开发,生产强制要求认证。
漏洞 2 — [HIGH] JWT Token 类型混淆(refresh token 当 access token 用)
位置: py/core/base/providers/auth.py:152-160 + r2r_auth.py:user()
auth_wrapper 和 R2RAuthProvider 的 user() 方法调用 decode_token() 校验 JWT,但都不检查 token_data.token_type == "access"。create_access_token 嵌入 token_type: "access"(~60 分钟),create_refresh_token 嵌入 token_type: "refresh"(7 天)。两者用同一 key 签名。decode_token(r2r_auth.py ~L110)只验证 token_type 存在,不限制其值。对比 refresh_access_token 正确检查 if token_data.token_type != "refresh"。
影响: 任何有效 refresh token 被当作 access token 用于所有 API 操作,有效认证窗口从 60 分钟延长到 7 天(DEFAULT_REFRESH_LIFETIME_IN_DAYS = 7),击败短时 access token 设计。攻击者获取 refresh token(XSS、泄露 cookie、日志)后无需调 /refresh 即获 7 天访问,且无副作用(无轮换、无撤销)可能惊动受害者。
复现:
# 攻击者获取受害者 refresh token:
curl "http://<host>/v3/users" -H "Authorization: Bearer <refresh_token>"
# decode_token 成功(签名有效、exp 未过期),token_type==refresh 未被检查,用户被查找并返回。修复: auth_wrapper/user() 校验 token_data.token_type == "access",拒绝 refresh token。
漏洞 3 — [HIGH] 改密/重置密码不撤销 token
位置: py/core/providers/auth/r2r_auth.py:310-330 (change_password, confirm_password_reset)
两者更新数据库 password_hash 但从不调用 token_handler.blacklist_token 撤销已签发 token。黑名单机制存在且在 logout() 和 refresh_access_token() 中正确使用。业务规则:用户改密(通常因怀疑被盗)或重置密码时,所有未过期 access token(最多 60 分钟)和 refresh token(最多 7 天)应失效。
影响: 已持有被盗 token 的攻击者在受害者改密后仍保留完整 API 访问至 token 自然过期(结合漏洞 2,refresh token 可达 7 天)。
复现:
- 攻击者通过
/login获取 access + refresh token。 - 受害者改密(
change_password)或重置(confirm_password_reset)。 - 数据库密码更新,但无 token 被黑名单。
- 攻击者继续用被盗 token 访问。
修复: change_password/confirm_password_reset 调用 blacklist_token 撤销该用户所有已签发 token。
汇总
| # | 漏洞 | 严重度 | 位置 |
|---|---|---|---|
| 1 | 认证绕过(默认 admin superuser) | CRITICAL | auth.py:122-138 |
| 2 | JWT token 类型混淆 | HIGH | auth.py:152-160 + r2r_auth.py |
| 3 | 改密不撤销 token | HIGH | r2r_auth.py:310-330 |
所有漏洞均已对照 main 分支源码核实。
Source: SciPhi-AI/R2R