Repository navigation
findData 从请求体里取执行上下文 —— context.isSystem 能整条跳过安全中间件(expand 本身已核实是干净的) #3960
Description
来自 #3946 留的那条尾巴:去查 expand 的"高级用法"(调用者可直传 Record<string, QueryAST>,每个子 AST 自带 object)是否是跨对象越权读通道。
先说 expand:干净的,而且防得相当到位
我的怀疑是错的,expandRelatedRecords(engine.ts:2361)四道防线都在:
- 目标对象不由调用者决定。 expand 的键必须是父对象 schema 上真实存在的字段,且带
reference、类型是lookup/master_detail/user;读的目标是fieldDef.reference—— 从 schema 来的。调用者子 AST 里那个object键根本没人读(和 全仓扫查"body 展开在受信任值之后":runtime 的 /data/:object/query 也中了,其余 8 处已核实无问题 #3946 里排除ast.object的理由一样)。 - 走引擎自己的
this.find(Security: $expand bypasses RLS/FLS on the referenced object (data leak on lookups) #2850 有意如此),所以被引用对象的 RLS + FLS 中间件照跑。 - id 谓词不会被顶掉 —— 子 AST 的
where是用显式$and合并的,注释里还专门写了"浅展开会 clobber 掉 id 约束",正是本系列的那个坑。 MAX_EXPAND_DEPTH = 3;__expandRead豁免面很窄(只find、只非 private 对象,只免掉对象级 CRUD/requiredPermissions 闸门,RLS 注入和 FLS 掩码照跑)。
结论:expand 无需改动。
但第 4 条那句"__expandRead 是服务端设的,executionContext 从不由客户端构造"不成立
packages/metadata-protocol/src/protocol.ts:2661:
const options: any = { ...request.query };
// …
if (request.context !== undefined) { // ← 条件赋值
options.context = request.context;
}request.query在每个入口上都是调用者的原始 bag —— REST 的POST /data/:object/query直接把req.body当query传进来(rest-server.ts:3733);context在下面的knownParams集合里,所以它也不会被扫进隐式过滤桶,原样留在options上;- 赋值是条件的,所以当没有服务端 context 解析出来时,调用者那个
context就是这次操作的执行上下文。
而它承载的东西是全部:
// plugin-security/src/security-plugin.ts:722
if (opCtx.context?.isSystem) {
return next(); // 整条 RLS / FLS / CRUD 链跳过
}__expandRead: true 同理能拿到上面那个豁免。两者在读路径上都不会被 schema 剥掉 —— ExecutionContextSchema.parse 只在 engine.createContext() 里用,读写热路径不走它。
协议层已实测确认
用真实的 ObjectStackProtocolImplementation + 假引擎,按匿名形状调用:
await p.findData({ object: 'invoice', query: { context: { isSystem: true, userId: 'root', __expandRead: true } } });
// 修复前 engine.find 收到的 options.context:
// { isSystem: true, userId: 'root', __expandRead: true } ← 原样可达性:端到端匿名复现我没做成,如实说明
拦在中间的是路由层 enforceAuth —— 默认 requireAuth: true 会先 401 掉匿名数据请求。要走通需要部署显式设 requireAuth: false(一个有文档、REST 插件启动时还会告警的 opt-out)。
我试过在 examples/app-crm/objectstack.config.ts 上加 api: { requireAuth: false } 起服务器实打,没生效 —— dev 命令这条路径没把 stack 的 api 键读到(serve.ts:1724 读 (config as any).api,但 dev 不走那段),四个探测都仍是 401。所以:
- 已实测确认:协议层会把请求体里的
context原样喂给引擎(上面那段); - 已代码确认:
isSystem短路整条中间件、读路径不做 schema 剥离; - 未实测:匿名数据请求在真实部署上确实可达 —— 这一环依赖
requireAuth: false,我没能在 dev CLI 上把这个开关打开。
所以这是一个被上层闸门挡住的 fail-open 默认,不是一个当场可打的洞。但协议层不该依赖上面那道闸门一直是开着的 —— 这正是它自己拥有的不变量。
修法
findData 在(条件)赋值之前无条件 delete options.context。执行上下文只能来自 request.context。
已核对不会破坏任何合法调用方:import-runner 的 findArgsBase(rest/src/import-runner.ts:245)把 context 放在顶层、不在 query 里;仓库里没有任何调用方从 query bag 里传 context。getData 等兄弟方法是从零构造 options 的,不受影响 —— 只有 findData 是"调用者 bag 就是 options bag"。
核对于 origin/main @ a225ef5。
Activity
fix(metadata-protocol): findData must not take its execution context …