Skip to content

bug/security: #2948 的 UPDATE readonly 剥离误伤 better-auth adapter 写入 → change-email / ban 等对 readonly sys_user 列的写入被静默丢弃 #3164

Description

@os-zhuang

概述

#2948/#3003引擎级 UPDATE readonly 剥离(engine.ts update 路径,!ctx.isSystem 时对 caller 提交的 readonly: true 字段剥离)会误伤 better-auth adapter 的写入。better-auth 的 adapter(plugin-auth/src/objectql-adapter.tswithSystemReadContext)对注入了 isSystem: true,但(update/delete)是无上下文透传:

// withSystemReadContext
insert: (m, d) => e.insert(m, d),   // 无 context
update: (m, d) => e.update(m, d),   // 无 context ← 关键
...
find/findOne/count: e.find(m, asSystem(q)),  // 有 isSystem

无 context 的 update 在引擎里命中 !opCtx.context?.isSystem(!undefined?.isSystem === true)→ 剥离生效。而 sys_user 的多个列声明为 readonly: true:email(line 499「change flows through better-auth change-email verification」)、banned/ban_reason/ban_expires(ADR-0092「written by the Ban User action」)等。因此 better-auth 经 adapter 对这些 readonly 列的写入会被静默剥离(HTTP 200 / 无报错,值不变——正是 #3003 式最难发现的静默形态)。

identity-write-guard.ts(ADR-0092 D2)会让 adapter 路径「passes untouched(其 engine 调用不带 caller context)」通过它自己的 guard,但紧接着引擎的 #2948 剥离仍会跑(它在 beforeUpdate hook 之后、按 suppliedKeys 剥离)——guard 放行 ≠ 剥离放行。

复现(机制级,已实证)

用真实引擎(ObjectQLPlugin,#2948 剥离在其 update 路径生效)+ capture 驱动,按 adapter 的确切调用形态 engine.update(model, { id, ...patch })(无第三参 / 无 context)更新 readonly 列:

ql.registry.registerObject({ name: 'ba_user', datasource: 'p', fields: {
  name:   { type: 'text' },
  email:  { type: 'text',    readonly: true },
  banned: { type: 'boolean', readonly: true },
}}, 'test', 'test');

await ql.update('ba_user', { id: 'rec-1', email: 'new@x.test', banned: true }); // 无 context = adapter 形态
// 驱动实际收到的 update payload:
//   { id: 'rec-1', updated_at: '…' }      ← email / banned 都被剥离
//   email present?  false
//   banned present? false

dogfood HTTP 层未能直接触发用户可见症状:/auth/change-email 在 dev harness 返回 CHANGE_EMAIL_DISABLED,/auth/admin/ban-user 未挂载(404)。即在这些 better-auth 功能启用的部署里症状才会显现;机制在引擎层已确证。

影响

任何经 better-auth adapter 写入 sys_user(及其它 managedBy: 'better-auth' 表)readonly 列的运行时流程:change-email(验证通过后写 email)、admin ban/unban(banned/ban_reason/ban_expires)、以及未来任何 admin-plugin 对 readonly 列的写入。表现为「操作返回成功但数据没变」。

备选修复

  1. adapter 写入按对象走 system 上下文(推荐,最小):withSystemReadContextupdate/delete 也注入 isSystem(与其 find/findOne/count 已有的处理对称)。理由:better-auth 是身份权威,它对自管表的写入本就是 system 写入;identity-write-guard 已把 user-context 写入拦在门外,adapter 路径只有 better-auth 自身的写入。
    • 需评估:使 adapter update 变 system 会同时绕过 FLS/owner-transfer 等——但对身份表的 better-auth 自写而言这是期望行为。
  2. 字段级 update 白名单扩展:复用 registerManagedUpdateWhitelist 思路,但那是给 user 写入开口的;adapter 是另一条路径,方案 1 更贴切。

判据 / 需核实

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecurity

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions