概述
ExecutionContext.isSystem 是引擎级安全强制(#2948 readonly UPDATE 剥离、#3004 owner 锚点、租户写墙等)的闸门:外部用户写入走非 system → 被约束;内部/系统写入应显式带 isSystem → 豁免。但大量核心内部写入方并不显式声明 isSystem,而靠「反正我不经过外部入口」隐式获得信任。
这条隐式信任已经反复咬人:
为什么重要(对齐「让 AI 写代码、少犯错」)
重构后的三步计划(替代「照单迁移 40 处」)
不要把这理解为「机械补 40 处 isSystem」——那无当下收益、且每次改都可能顺带翻转某表的 FLS/owner 行为(把写入方改 system 会绕过这些),为了少犯错反而可能犯错。按下面三步、按安全相关性排序:
步骤 1 —— 立「可检查的契约」(最高杠杆,但需选对工具)
目标:框架内部包里对业务表的 engine.insert/update/delete,要么显式带 isSystem,要么显式标注面向用户,新增违例在 CI / 发布期失败。
⚠️ 实现须知(本 issue 探索得到的关键结论):这不能用 check-role-word.mjs 那种正则计数 ratchet 来做。「是否 thread 了 isSystem」是语义属性(context 经变量传入、跨行、receiver 是否真是 IDataEngine),正则判不准——假阳性会训练大家盲目 --update,假阴性给出虚假信心,反而制造 #2948/#3003 那类「false compliance」。可行形态二选一:
- (a) AST / 类型感知的 lint(用 ts-morph / typescript 分析:receiver 类型是
IDataEngine,第三参 options 的 context 是否含 isSystem),配 baseline ratchet 冻结现有 ~40 处、只挡新增。工作量真实,但一次到位。
- (b) 类型/API 层强制:让引擎写方法要求一个显式的 actor 参数(
{ actor: 'system' | ExecutionContext }),使「不声明身份就调不通」由编译器保证,无需 lint。最干净,但触及所有调用点,改动更大。
步骤 2 —— 增量迁移(在契约之后)
按碰 readonly / owner / 审批类列的写入方优先迁移(better-auth adapter 已在 #3164 完成),每个配测试。碰不到安全相关列的写入方不必为统一而改。
步骤 3 —— 收敛强制
覆盖够高后,把 #3043 的 INSERT 剥离从入口下沉回引擎、删掉入口特例,两处合一,并堵上「插件 hook 里非 system 写入」这类入口拦不到的引擎内路径。
当前判定
关联
概述
ExecutionContext.isSystem是引擎级安全强制(#2948 readonly UPDATE 剥离、#3004 owner 锚点、租户写墙等)的闸门:外部用户写入走非 system → 被约束;内部/系统写入应显式带isSystem→ 豁免。但大量核心内部写入方并不显式声明isSystem,而靠「反正我不经过外部入口」隐式获得信任。这条隐式信任已经反复咬人:
readonlyon INSERT at the data-write ingress (#3043) #3162):想把 readonly 的 INSERT 剥离做在引擎级(与 UPDATE 对称)时,发现better-auth adapter/metadata-repo(event_seq/id)/seed-loader等核心写入方都非 system 播种 readonly 列,引擎级一剥就崩(dev-admin 登录、元数据事件日志 NOT NULL)——粗测 core/platform 约 40+ 处同类调用点。只好把 INSERT 剥离改到外部入口,留下「引擎两处强制 + 入口特例」的不对称。为什么重要(对齐「让 AI 写代码、少犯错」)
isSystem才是可审计原语。重构后的三步计划(替代「照单迁移 40 处」)
不要把这理解为「机械补 40 处 isSystem」——那无当下收益、且每次改都可能顺带翻转某表的 FLS/owner 行为(把写入方改 system 会绕过这些),为了少犯错反而可能犯错。按下面三步、按安全相关性排序:
步骤 1 —— 立「可检查的契约」(最高杠杆,但需选对工具)
目标:框架内部包里对业务表的
engine.insert/update/delete,要么显式带isSystem,要么显式标注面向用户,新增违例在 CI / 发布期失败。check-role-word.mjs那种正则计数 ratchet 来做。「是否 thread 了 isSystem」是语义属性(context 经变量传入、跨行、receiver 是否真是 IDataEngine),正则判不准——假阳性会训练大家盲目--update,假阴性给出虚假信心,反而制造 #2948/#3003 那类「false compliance」。可行形态二选一:IDataEngine,第三参 options 的context是否含isSystem),配 baseline ratchet 冻结现有 ~40 处、只挡新增。工作量真实,但一次到位。{ actor: 'system' | ExecutionContext }),使「不声明身份就调不通」由编译器保证,无需 lint。最干净,但触及所有调用点,改动更大。步骤 2 —— 增量迁移(在契约之后)
按碰 readonly / owner / 审批类列的写入方优先迁移(better-auth adapter 已在 #3164 完成),每个配测试。碰不到安全相关列的写入方不必为统一而改。
步骤 3 —— 收敛强制
覆盖够高后,把 #3043 的 INSERT 剥离从入口下沉回引擎、删掉入口特例,两处合一,并堵上「插件 hook 里非 system 写入」这类入口拦不到的引擎内路径。
当前判定
readonlyon INSERT at the data-write ingress (#3043) #3162 的入口方案已让 readonly 在各路径都被强制;唯一的活 bug(bug/security: #2948 的 UPDATE readonly 剥离误伤 better-auth adapter 写入 → change-email / ban 等对 readonly sys_user 列的写入被静默丢弃 #3164)已修。可稳稳搁置。关联
readonlyon INSERT at the data-write ingress (#3043) #3162(入口级 INSERT 剥离;blast radius 是本 issue 的动因)!isSystem为门)