来源:#6551(dispatcher 面 share-links 整体透传信封,PR #6647)实施时的越界发现。未评级——交分诊,只给实读事实。与 #6551 的修复无因果:修前修后该缺陷都在,只是修后授权输入齐了,常规请求不再触发;真正无权(集合确实没有 allowRead)的调用者仍会命中。
事实(核于 origin/main e39dd66e7)
packages/runtime/src/domains/share-links.ts 的统一 catch(文件末尾):
} catch (err: any) {
return sendErr(err?.status ?? 500, err?.code ?? 'INTERNAL', err?.message ?? 'Share link request failed');
}
只读 err?.status。而这条路径上真实会飞出来的拒绝错误带的是 statusCode:
packages/plugins/plugin-security/src/errors.ts:8-10 — PermissionDeniedError { code = 'PERMISSION_DENIED'; statusCode = 403 }(无 status 字段);runtime 自己的镜像类 packages/runtime/src/security/resolve-execution-context.ts 同形。
- 触发链:
svc.createLink 的 [Finding-2] 可见性读 engine.find(object, { context }) 走安全中间件;CRUD 门 / 两门写门 / tenant CHECK 拒绝时 throw PermissionDeniedError → ShareLinkService 不接 → 冒到 share-links 的 catch → err?.status 为 undefined → HTTP 500,code 却如实取到 PERMISSION_DENIED。一个 403 类拒绝以 5xx 信封出门(且 5xx 路径上 looksLikeInternalErrorLeak 还会把 [Security] Access denied… 的消息脱敏掉,进一步遮住真因)。
为什么这不是孤例写法的锅
dispatcher 已有共享的映射器 errorFromThrown(packages/runtime/src/http-dispatcher.ts:718),它同时读 status 与 statusCode;share-links 域用的是自己的手写 catch,没走它。同族对照:#5582 记的是 rest 侧 mapDataError 的状态直通不对等——家族相同(生产者声明的状态拿不回来),表面不同(rest vs dispatcher domain),互不覆盖。
复现草图
group/single 任一姿态,给调用者一个对目标对象没有 allowRead 的 permission set,POST /api/v1/share-links 指向该对象任一记录 → 观测 500 + PERMISSION_DENIED(期望 403)。#6551 落地的测试架子(packages/runtime/src/domains/share-links-enforcement-context.test.ts,真 SecurityPlugin + 真 ShareLinkService)去掉集合的 allowRead 即可直接复现。
建议方向(实施者定夺)
catch 改走 errorFromThrown(或至少 err?.status ?? err?.statusCode ?? 500)。前者顺带把 issues/fields 结构化细节也接住,与 /meta 域一致。
未指派、未标签——由 triage 席分诊。
来源:#6551(dispatcher 面 share-links 整体透传信封,PR #6647)实施时的越界发现。未评级——交分诊,只给实读事实。与 #6551 的修复无因果:修前修后该缺陷都在,只是修后授权输入齐了,常规请求不再触发;真正无权(集合确实没有 allowRead)的调用者仍会命中。
事实(核于
origin/maine39dd66e7)packages/runtime/src/domains/share-links.ts的统一 catch(文件末尾):只读
err?.status。而这条路径上真实会飞出来的拒绝错误带的是statusCode:packages/plugins/plugin-security/src/errors.ts:8-10—PermissionDeniedError { code = 'PERMISSION_DENIED'; statusCode = 403 }(无status字段);runtime 自己的镜像类packages/runtime/src/security/resolve-execution-context.ts同形。svc.createLink的 [Finding-2] 可见性读engine.find(object, { context })走安全中间件;CRUD 门 / 两门写门 / tenant CHECK 拒绝时 throwPermissionDeniedError→ShareLinkService不接 → 冒到 share-links 的 catch →err?.status为 undefined → HTTP 500,code却如实取到PERMISSION_DENIED。一个 403 类拒绝以 5xx 信封出门(且 5xx 路径上looksLikeInternalErrorLeak还会把[Security] Access denied…的消息脱敏掉,进一步遮住真因)。为什么这不是孤例写法的锅
dispatcher 已有共享的映射器
errorFromThrown(packages/runtime/src/http-dispatcher.ts:718),它同时读status与statusCode;share-links 域用的是自己的手写 catch,没走它。同族对照:#5582 记的是 rest 侧mapDataError的状态直通不对等——家族相同(生产者声明的状态拿不回来),表面不同(rest vs dispatcher domain),互不覆盖。复现草图
group/single任一姿态,给调用者一个对目标对象没有allowRead的 permission set,POST/api/v1/share-links指向该对象任一记录 → 观测 500 +PERMISSION_DENIED(期望 403)。#6551 落地的测试架子(packages/runtime/src/domains/share-links-enforcement-context.test.ts,真 SecurityPlugin + 真 ShareLinkService)去掉集合的allowRead即可直接复现。建议方向(实施者定夺)
catch 改走
errorFromThrown(或至少err?.status ?? err?.statusCode ?? 500)。前者顺带把issues/fields结构化细节也接住,与/meta域一致。未指派、未标签——由 triage 席分诊。