实测于 origin/main@eb26126d5,在核验 #5701(#4706 裁决 B 案契约半边)的前置前提时发现。独立缺陷,不依赖任何裁决即可修;同时它是 #5702「$regex 响亮拒收」落地前必须先排掉的那块。
测到的东西
packages/plugins/plugin-auth/src/objectql-adapter.ts:137,convertWhere() 的最后一支:
} else if (condition.operator === 'contains') {
filter[fieldName] = { $regex: condition.value };
}
condition.value 是 better-auth 传下来的未转义比较值,直接坐进 $regex 的模式位。它随后的命运按 driver 分叉:
| driver |
求值方式 |
结果 |
driver-memory |
new RegExp(target, condition.$options || '')(memory-matcher.ts:209-213) |
当真正则跑 —— 元字符全部生效 |
driver-sql / driver-sqlite-wasm |
编成子串 LIKE '%v%'(sql-driver.ts:6511,走 applyLike,%/_/\ 有转义) |
子串匹配,元字符是字面量 |
driver-turso |
同上(local 继承 SqlDriver;remote 见 remote-transport.ts:1258) |
子串匹配 |
也就是说 better-auth 的一个 contains 查询:
- 在内存 driver 上,
a.b 会命中 axb(. 是通配),^x 会锚定,café 之类无元字符的值才碰巧正确;
- 在 SQL 上,同样的值是纯子串;
- 模式非法时(如值里带单个
(),memory-matcher 的 catch 返回 false —— 静默不匹配,不报错。
这正是 #4706 正文描述的缺陷形态,只是发生在认证路径上,而且比较值来自用户输入。app 级测试若跑在内存替身上、生产跑 SQL,两边结果不同(hotcrm#630 的形状)。
driver-sql / driver-turso / driver-memory / objectql(having-filter.ts:37)四处的 $regex 支持都是为这一个生产者存在的,代码里已写明。driver-memory/src/filter-refusal.ts:154-158 原话:
$regex —— not in FILTER_OPERATORS, but really produced: plugin-auth's ObjectQL adapter emits { field: { $regex: value } } for a contains search. ... Refusing it here would break a live producer.
所以 #5702 的「校验期响亮拒收 $regex」在这一行翻转之前落地,会直接打断 better-auth 的认证查询。顺序:先本单,后拒收。
建议的修法(contract-first)
把这一支改成词表里真有的算子:
} else if (condition.operator === 'contains') {
filter[fieldName] = { $contains: condition.value };
}
$contains 在 FILTER_OPERATORS(spec/src/data/filter.zod.ts:957-968)里,五后端都实现,且 SQL 侧的 applyLike 已带 %/_/\ 转义与显式 ESCAPE —— 元字符回归字面量,正是 better-auth 的 contains 本意。
一处需要在实施时确认的语义差:$contains 的大小写行为今天各后端不一(memory/formula 敏感、SQLite/turso ASCII 不敏感、mongo 不敏感、PG 敏感;spec 在 filter.zod.ts:139 明写「Case sensitivity should be handled at backend level」)。相对现状而言这个差不是本单引入的 —— 裸 $regex 在 memory 上本来也是大小写敏感的,所以翻转对该 driver 是等价的;但若 #5701 的 Q2(是否同批钉死 $contains 大小写语义)有裁决,本单应与之对齐。
验证建议
翻转后应有一条 pin:一个带元字符的比较值(如 a.b)经 better-auth contains 路径,在内存 driver 上不命中 axb —— 反向验证方向是「先红后绿」(翻转前该断言红,因为裸 $regex 会命中)。
相关:#4706(算子名实不符母单)、#5701(契约半边)、#5702(驱动半边拒收)。
实测于
origin/main@eb26126d5,在核验 #5701(#4706 裁决 B 案契约半边)的前置前提时发现。独立缺陷,不依赖任何裁决即可修;同时它是 #5702「$regex响亮拒收」落地前必须先排掉的那块。测到的东西
packages/plugins/plugin-auth/src/objectql-adapter.ts:137,convertWhere()的最后一支:condition.value是 better-auth 传下来的未转义比较值,直接坐进$regex的模式位。它随后的命运按 driver 分叉:driver-memorynew RegExp(target, condition.$options || '')(memory-matcher.ts:209-213)driver-sql/driver-sqlite-wasmLIKE '%v%'(sql-driver.ts:6511,走applyLike,%/_/\有转义)driver-tursoremote-transport.ts:1258)也就是说 better-auth 的一个
contains查询:a.b会命中axb(.是通配),^x会锚定,café之类无元字符的值才碰巧正确;(),memory-matcher的catch返回false—— 静默不匹配,不报错。这正是 #4706 正文描述的缺陷形态,只是发生在认证路径上,而且比较值来自用户输入。app 级测试若跑在内存替身上、生产跑 SQL,两边结果不同(hotcrm#630 的形状)。
为什么它同时挡着 #5702
driver-sql/driver-turso/driver-memory/objectql(having-filter.ts:37)四处的$regex支持都是为这一个生产者存在的,代码里已写明。driver-memory/src/filter-refusal.ts:154-158原话:所以 #5702 的「校验期响亮拒收
$regex」在这一行翻转之前落地,会直接打断 better-auth 的认证查询。顺序:先本单,后拒收。建议的修法(contract-first)
把这一支改成词表里真有的算子:
$contains在FILTER_OPERATORS(spec/src/data/filter.zod.ts:957-968)里,五后端都实现,且 SQL 侧的applyLike已带%/_/\转义与显式ESCAPE—— 元字符回归字面量,正是 better-auth 的contains本意。一处需要在实施时确认的语义差:
$contains的大小写行为今天各后端不一(memory/formula 敏感、SQLite/turso ASCII 不敏感、mongo 不敏感、PG 敏感;spec 在filter.zod.ts:139明写「Case sensitivity should be handled at backend level」)。相对现状而言这个差不是本单引入的 —— 裸$regex在 memory 上本来也是大小写敏感的,所以翻转对该 driver 是等价的;但若 #5701 的 Q2(是否同批钉死$contains大小写语义)有裁决,本单应与之对齐。验证建议
翻转后应有一条 pin:一个带元字符的比较值(如
a.b)经 better-authcontains路径,在内存 driver 上不命中axb—— 反向验证方向是「先红后绿」(翻转前该断言红,因为裸$regex会命中)。相关:#4706(算子名实不符母单)、#5701(契约半边)、#5702(驱动半边拒收)。