立卡门 ①:有具名落点与复现的产品缺陷。finding 类别 b(写检查与读在同一策略上给出两个答案)。reach: 经引擎写入门(ObjectQL.insert 加 SecurityPlugin)实测,读侧是公开数据门的同一条路径。
动手的读者:分诊定级并定车道。写检查在 plugin-security(domain:services),拒收规则的源头在 @objectstack/core。
查重:mcp__github__search_issues 查 "RLS write check scalar comparison multi-valued JSON column admitted read refuses INVALID_FILTER policy",42 条命中(含 closed),都不是本题。同族已关的有 #20355 / #20347(跨比较类:写检查放行而读拒收,这是另一个比较类)和 #20212(比较值形状)。
来源
#21238 的 dev 报告 5942236028(PR #21253,out_of_scope_findings[0])。实测在 be5a83cf 与 8f7c43bf 上,覆盖 driver-sql(better-sqlite3)和 driver-sqlite-wasm,以持有权限集的成员身份,using 与 check 用同一谓词,字段 tags 声明为多值。
reach:
| 策略 |
成员写入 |
存储 |
同一策略的读 |
record.tags != 'x' |
['x'](PR #21253 之后 'x' 也一样)放行 |
["x"] |
400 INVALID_FILTER |
!(record.tags in ['x']) |
放行 |
["x"] |
400 |
record.tags == 'x' |
403 |
— |
400 |
record.tags in ['x'] |
403 |
— |
400 |
record.tags > 'a' |
400 |
— |
400(匹配器本来就拒收排序) |
前两行:读侧拒收的策略,在写侧被求值并放行了一行。
落点(源码读)
- 读侧的拒收集:
@objectstack/core 的 JSON_COLUMN_INCOMPATIBLE_OPERATORS,即 json-column-operator-refusal.ts。模块注释写明 "Two faces, one rule":进程内的两个面(objectql 的按聚合 filter、driver-memory 的 filter 门)在同样声明的字段上拒收这些算子。
- 写检查:
plugin-security 的 rls-check-stored-form.ts / storedFormCheckJudge,经 @objectstack/formula 的 matchesFilterCondition 求值。它也是一个进程内的面,却照常求值这些算子,没有拒收。
- 未测量: 权限集的编写门是否会拒收这样的策略。
方向(供分诊参考,不是裁决)
- 写检查在已声明 JSON 存储 / 多值的列上,遇到
JSON_COLUMN_INCOMPATIBLE_OPERATORS 里的算子,与读侧给出同一拒收(同一个 core 规则,⛔ 不另写副本)。或者在编写门就拒收这类策略,让它根本不会存在。由分诊选择,或两者都做。
- Pins: 上表前两行的写入不再放行;与读侧同码同状态,或在编写门被拒;
contains / notContains 不变(对照)。
依赖
在 PR #21253(#21238)落地之后进行:它把多值列带进 storedFormCheckJudge。
Generated by Claude Code · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
立卡门 ①:有具名落点与复现的产品缺陷。
finding类别 b(写检查与读在同一策略上给出两个答案)。reach:经引擎写入门(ObjectQL.insert加SecurityPlugin)实测,读侧是公开数据门的同一条路径。动手的读者:分诊定级并定车道。写检查在
plugin-security(domain:services),拒收规则的源头在@objectstack/core。查重:
mcp__github__search_issues查 "RLS write check scalar comparison multi-valued JSON column admitted read refuses INVALID_FILTER policy",42 条命中(含 closed),都不是本题。同族已关的有 #20355 / #20347(跨比较类:写检查放行而读拒收,这是另一个比较类)和 #20212(比较值形状)。来源
#21238 的 dev 报告
5942236028(PR #21253,out_of_scope_findings[0])。实测在be5a83cf与8f7c43bf上,覆盖 driver-sql(better-sqlite3)和 driver-sqlite-wasm,以持有权限集的成员身份,using与check用同一谓词,字段tags声明为多值。reach:record.tags != 'x'['x'](PR #21253 之后'x'也一样)放行["x"]400 INVALID_FILTER!(record.tags in ['x'])["x"]400record.tags == 'x'403400record.tags in ['x']403400record.tags > 'a'400400(匹配器本来就拒收排序)前两行:读侧拒收的策略,在写侧被求值并放行了一行。
落点(源码读)
@objectstack/core的JSON_COLUMN_INCOMPATIBLE_OPERATORS,即json-column-operator-refusal.ts。模块注释写明 "Two faces, one rule":进程内的两个面(objectql 的按聚合 filter、driver-memory 的 filter 门)在同样声明的字段上拒收这些算子。plugin-security的rls-check-stored-form.ts/storedFormCheckJudge,经@objectstack/formula的matchesFilterCondition求值。它也是一个进程内的面,却照常求值这些算子,没有拒收。方向(供分诊参考,不是裁决)
JSON_COLUMN_INCOMPATIBLE_OPERATORS里的算子,与读侧给出同一拒收(同一个 core 规则,⛔ 不另写副本)。或者在编写门就拒收这类策略,让它根本不会存在。由分诊选择,或两者都做。contains/notContains不变(对照)。依赖
在 PR #21253(#21238)落地之后进行:它把多值列带进
storedFormCheckJudge。Generated by Claude Code ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ