better-auth:一个OAuthstate,为什么能当MagicLink登录别人"

admin 2026-10-30 13:56:28 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

better-auth:一个 OAuth state,为什么能当 Magic Link 登录别人

原创

云梦DC
云梦DC

云梦安全

2026年10月30日 08:44
河南

去阅读

2026 年 10 月披露的 CVE-2026-114607(GHSA-965c-763c-88jm,CVSS 9.1)影响 better-auth 1.4.0-beta.18 到 1.7.7 之前的版本。问题出在两类临时令牌共用了一张表:OAuth 登录产生的 state,可以被 Magic Link 的验证接口当成登录 token 消费。

本地用 1.7.6 发起一次 OAuth 登录,拿到 32 位 state,没有访问 GitHub,也没有完成回调。把这个值拼进 /magic-link/verify?token=... 后,服务端返回 302,并下发 better-auth.session_token。随后请求 get-session,返回的是预先建好的 [email protected]。

漏洞速览

项目 内容
漏洞编号 CVE-2026-114607 / GHSA-965c-763c-88jm
影响组件 better-auth >= 1.4.0-beta.18, < 1.7.7
修复版本 1.7.7
漏洞类型 认证令牌类型混淆(CWE-287)
危害等级 CVSS 9.1 Critical
触发前提 开启 Magic Link 与社交/Generic OAuth;OAuth state 存数据库;Magic Link 令牌默认明文存储

两条流程写进了同一张表

Magic Link 的流程不复杂。用户提交邮箱,服务端生成随机 token,把 {email, name} 和过期时间写进 verification 表,再把带 token 的链接发到邮箱。用户点击后,服务端按 token 查表,取出邮箱,建立会话。

默认 storeToken: \"plain\" 时,记录大致是这样:

verification
  identifier = <token>
  value      = {\"email\":\"[email protected]\",\"name\":\"...\"}
  expiresAt  = now + 5min

OAuth 登录也需要一个 state 防 CSRF。当 account.storeStateStrategy 为 \"database\" 时,better-auth 同样往 verification 表写一行:

verification
  identifier = <state>
  value      = {\"callbackURL\":\"/\",\"codeVerifier\":\"...\",\"oauthState\":\"<state>\",...}
  expiresAt  = now + 10min

两条流程单独看都没有明显问题:随机值够长、有有效期、消费一次就失效。缺陷在查表方式。Magic Link 验证只按 identifier 找记录,没有确认这条记录是哪条流程写进来的。OAuth state 恰好也落在同一个字段里。

/sign-in/social 接受一个 additionalData 字段,原本是让业务把自定义信息带过 OAuth 流程。这个对象会被展开进 state 的 value。攻击者在发起 OAuth 时可以写入目标邮箱:

{
  \"provider\": \"github\",
  \"callbackURL\": \"/\",
  \"additionalData\": {
    \"email\": \"[email protected]\",
    \"name\": \"Victim User\"
  }
}

服务端返回的授权 URL 里带 state。攻击者不用去 GitHub,也不用拿授权码,只要把这个值换成 Magic Link 的 token:

GET /api/auth/magic-link/verify?token=<state>&callbackURL=/

验证逻辑拿 token 去 verification 表按 identifier 查,正好命中刚才那条 OAuth state。接着它把记录的 value 当成 Magic Link 载荷,取出 email,找到用户,创建会话并下发 cookie。攻击者拿到的会话和正常登录没有区别。

本地复现的结果

实验用 better-sqlite3 做内存数据库,1.7.6 和 1.7.7 各跑一遍。两边配置相同的 GitHub provider,client id 和 secret 都是虚构值;Magic Link 插件开启,OAuth state 存数据库。测试前先写入一个 emailVerified: true 的 victim 账号,模拟已经存在的正常用户。

1.7.6 的步骤和结果:

POST /api/auth/sign-in/social              -> 200,授权 URL 内含 32 位 state
GET  /api/auth/magic-link/verify?token=... -> 302,下发 better-auth.session_token
GET  /api/auth/get-session                 -> [email protected], emailVerified=true

把脚本切到 1.7.7,第二步变成 302 /?error=INVALID_TOKEN,没有 set-cookie;第三步返回 null。state 的长度和有效期没有变,变化是两类记录被分到了不同的命名空间。

关闭注册并不能挡住

公告专门提醒,Magic Link 的 disableSignUp: true 只能阻止新建账号,挡不住接管已有账号。上面的复现登录的就是一个早已存在的用户,注册开关全程没有参与。

缩短 state 有效期、增加随机位数也没有解决问题。攻击者拿到 state 后可以立即使用,几分钟的窗口足够完成请求。根因是消费令牌时没有确认签发流程,跟随机值强度不是一回事。

日志里怎么串事件

单看一条日志,这次攻击很像正常的 Magic Link 登录。需要把几个动作放在一起:

  • 同一 IP 或会话先调用 /sign-in/social,短时间内又调用 /magic-link/verify;
  • verification 表里出现 identifier 与 OAuth state 相同、却被 Magic Link 路径消费的记录;
  • 用户没有请求过 Magic Link,却多出一条 method=magic-link 的会话;
  • 登录后马上修改邮箱、密码、MFA 或 API key。
AI辅助Ransomware操作曝光" 网络安全文章

AI辅助Ransomware操作曝光"

该文档曝光了n0n勒索软件操作者的服务器基础设施,揭示其利用AI编码代理环境进行攻击,涉及23个受害者分布于14个国家。攻击者通过凭证访问方式入侵,包括FortiGateSSL-VPN喷洒、暴露的AWS密钥和弱RDP,并劫持受害者CiscoFMC切断ISP网络。此外,攻击者擦除S3存储桶和备份作为勒索杠杆,且n0n锁加密在受害者间重复使用硬编码密钥。建议加强凭证管理和备份安全防护。
评论:0   参与:  0