Repository navigation
dispatcher 的 /auth 域内置 mock 登录:无 auth 服务时任意邮箱+任意密码都返回 200 + 伪造 session token —— 在发行装配里,不是 dev-only #4113
Description
按 Prime Directive #10 记录,#4093 A 档准备工作中发现。这条比 #4093 讨论的所有 plugin-dev stub 都严重,因为它不在 dev 插件里,而在 packages/runtime 的 dispatcher 里。
现象
packages/runtime/src/domains/auth.ts:
const authService = await deps.getService(CoreServiceName.enum.auth);
if (authService && typeof authService.handler === 'function') {
return { handled: true, result: await authService.handler(...) };
}
// 2. Mock fallback for MSW/test environments when no auth service is registered
return mockAuthFallback(normalizedPath, method, body);mockAuthFallback 对 POST /api/v1/auth/sign-in/email(以及 login / sign-up/email / register):
const id = `mock_${randomUUID()}`;
return { handled: true, response: { status: 200, body: {
user: { id, name: 'Mock User', email: body?.email || 'mock@test.local', emailVerified: true, … },
session: { id: `session_${id}`, userId: id, token: `mock_token_${id}`,
expiresAt: new Date(Date.now() + 86_400_000).toISOString() },
}}};密码根本没有被读取。任意邮箱 + 任意密码(或不带密码)→ 200 + 一个看起来完全正常的 user + session token + 24 小时有效期。
触发条件(不是 dev-only)
只要 auth 槽位里没有带 .handler 方法的服务。具体地:
os serve --preset minimal——TIER_PRESETS.minimal = ['core'],不含auth;- 任何嵌入 dispatcher 但没装
@objectstack/plugin-auth的宿主 —— 这正是 Route parity: discovery-advertised routes must be reachable on the hono server (os serve/dev) — declared === enforced #3369 /@objectstack/restadvertisesmcpin /discovery with no way to serve it — declared ≠ enforced, and the route-parity gate'smcp: falseseam is never exercised #4024 已经认定为真实失效模式的那类("an embedder that mounts the dispatcher without plugin-mcp"); - 以及现在的 dev 栈:plugin-dev 的 auth stub 实现的是
handleRequest,不是handler,所以那个 stub 从来没被这个分支调用过 —— 占着槽位、被 discovery 报成stub,而真正应答/auth/*的一直是这个 mock。
严重性
不是越权:mock 发出的 token 没有任何 session 存储支撑,身份解析走的是 authService.api.getSession()(packages/runtime/src/security/resolve-execution-context.ts:104-127),没有 auth 服务就返回 undefined → 请求保持匿名 → shouldDenyAnonymous 照常拒绝数据访问。所以拿着 mock_token_* 读不到任何数据。
但它是对客户端撒谎,而且是最不该撒谎的那件事:
- 客户端(console / SDK / agent)拿到 200 + session token,会认为登录成功;随后每个请求却是匿名的 —— "登录成功了但什么都读不到",而且没有任何一层告诉它为什么。
POST /auth/sign-in/email用任意密码返回 200,是任何安全扫描/渗透测试都会直接标红的形状,无论后端是否真的放行。- 它违反的正是 ADR-0076 D12 从 降级 analytics shim 把 ExecutionContext 丢在门口 ——
/analytics/query在没装 service-analytics 的装配里不注入 RLS/租户谓词,契约字段where也被静默忽略 #3891 学到的那条:"A fallback may degrade features, never security semantics" —— 而且 降级 analytics shim 把 ExecutionContext 丢在门口 ——/analytics/query在没装 service-analytics 的装配里不注入 RLS/租户谓词,契约字段where也被静默忽略 #3891 的对象(analytics shim)只是丢了 RLS 作用域,这一条是伪造认证成功。 - 理由写的是"for MSW/test environments",却没有任何环境判据 —— 只判"auth 槽位空不空",然后跟着发行版一起进生产。
依赖面:查下来没有真实消费者
- 本仓:只有两处测试它自己的用例(
http-dispatcher.test.ts:907、domain-handler-registry.test.ts:528-531,后者断言token匹配/^mock_token_/)。 ../objectui:登录相关测试全部在 HTTP 客户端层自己 mock(packages/auth/src/__tests__/createAuthClient.test.ts等),不经过 dispatcher 的这个 fallback。
也就是说,那句 "MSW/browser-only environments" 的理由,我在两个仓库里都找不到实际依赖它的消费者 —— 只有为它自己写的测试在支撑它。
建议
按 #3891 的先例走:退役这个 mock,空槽答 404。
/auth/*没有 auth 服务时 404,与 analytics 空槽同形 —— 这是诚实信号("没装 auth 插件"),而不是伪造一次成功登录。- discovery 已经在正确地报了:
routes.auth按auth服务是否注册来广告,空槽时就是unavailable。目前的矛盾是 discovery 说没有、/auth/*却答 200 —— 一个declared ≠ enforced的反向实例。 - 那两处测试改成钉住新的诚实行为(404),而不是钉住 mock。
- 如果真有浏览器纯前端 mock 的需求,那应该是 MSW handler 或显式 opt-in 的 mock 插件(
OS_ALLOW_*级别的显式开关),不是"槽位空了就自动伪造"。
顺带:plugin-dev 的 auth dev stub 在这条修掉之前删也没有意义(它本来就没被 /auth 域调用)。#4093 A 档里的 auth 一项因此挂在这条上,我在那边也标了。
Activity
fix(plugin-dev,plugin-hono-server): retire the security dev stubs; re…
Merge origin/main — adopt #4126's canonical guard shape; the wholesal…
fix(runtime): /ui discovery gates on the protocol service that serves…
os-zhuang commentedon Jul 30, 2026
结论收敛为:删除,不做开关
原文给了两个选项(退役 / 或改成显式 opt-in 的 mock 插件)。基于此后落地的东西,第二个选项应当排除,本 issue 的处置就是删掉相关代码:
/analytics/query在没装 service-analytics 的装配里不注入 RLS/租户谓词,契约字段where也被静默忽略 #3891(analytics shim)、plugin-dev 的 analytics dev stub 仍会填满槽位,dispatcher 不检查 handlerReady —— #3891 退役 shim 后同形状的最后一处残留(dev-only) #4000(dev analytics stub)、dispatcher 其余服务域仍只判槽位占用、不读 handlerReady —— #4000 在 analytics 一域落地后剩下的类推面 #4058/fix(runtime,metadata-protocol): every dispatcher domain gates on handlerReady, not slot occupancy (#4058 step 2) #4086(automation/notification/ai)、fix(plugin-dev,plugin-hono-server): retire the security dev stubs; refuse to load under NODE_ENV=production (#4093) #4126(security ×3)、feat(plugin-dev)!: retire the stub table — DevPlugin assembles real plugins, empty slots stay empty (ADR-0115) #4137(整张表)。../objectui的登录测试全部在 HTTP 客户端层自己 mock。一个重要更新:#4143(源自 #4127)已经缩小了它的射程
原文说它"判据只有 auth 槽位有没有服务"—— 当时更糟。#4143 之前,分支 1 探测的是
authService.handler(request, response),没有任何实现有这个方法(契约声明的是handleRequest(request)),所以分支 1 永远为假:即使注册了真实 auth 服务,/auth/*拿到的也是 mock 的mock_<uuid>session,而不是真实认证。#4143 按契约修好了探测,真实服务现在会赢。所以现在 mock 的射程精确地就是"auth 槽位为空",而这恰好是删除正确、且 404 诚实的那种情况 —— discovery 在该情况下本来就报
auth: unavailable且不广告routes.auth。删除清单(核实于
dd5daac)packages/runtime/src/domains/auth.ts共 141 行,其中约 **95 行(2/3)**是 mock 及其专属助手:mockAuthFallback()randomUUID()http-dispatcher.test.tsdescribe('handleAuth mock fallback (MSW/test mode)')domain-handler-registry.test.ts/auth mock fallback serves sign-up…(断言token匹配/^mock_token_/)handleAuthRequest保留分支 1(真实服务),空槽改为{ handled: false }。删除后该文件约 45 行。一处留给维护者的判断:空槽应答用
handled: false→ 宿主 404(与 discovery 的auth: unavailable+ 不广告路由一致,同 #4000 后的/analytics),还是像/storage、/i18n那样给显式 501。我倾向 404 —— 路由既没被广告也没被挂载,404 就是那个事实;但 501 在"这个服务端没实现认证"的表达上更明确。plugin-dev 的
authstub 不在清单里 —— #4137 随整张表已经删掉了。Generated by Claude Code
fix(runtime)!: the /auth domain answers 501 instead of fabricating a …
os-zhuang commentedon Jul 30, 2026
已由 #4179(
5b08389)修复并关闭 —— 删除,不加开关。空槽(以及占着槽位但不实现契约
handleRequest的 wrong-shaped occupant)现在答:为什么是 501 不是 404:
/i18n是形状上最近的先例(核心能力、dispatcher 自有域、背后可选插件、空槽时 discovery 同样不广告路由),它选的就是 501。路由确实挂着,缺的是背后的实现 —— 这正是 501 陈述的事实,404 会把它描述成"路径不存在",把运维引向一个不存在的路由 bug。而且它保住了 mock 当初唯一正确的那个观察:那段注释说自己存在是为了让登录流程"不要 404" —— 担忧是对的,只是用撒谎回答;501 在不撒谎的前提下回答同一个担忧,并点名补救办法。顺带清掉两处 mock 的残留:
randomUUID助手 —— 一个纯粹为铸造 mock session id 而存在的 CSPRNG(注释原话 "these ids feed mock session tokens"),mock 一走即死代码。domains/auth.ts141 → 81 行。auth.ts: handBuilt: 4豁免降为 0。那 4 个 better-auth 形状的 body 是 mock 的、不是桥接的(真实服务返回它自己的Response,在这个文件里一个 body 都不拼)—— CI 直接以found 0, declared 4拦下,棘轮按设计生效。content/docs/permissions/authentication.mdx原本有一整节把 6 个伪造端点当特性列表,现已改写为 501 说明 + 两条真实出路(在 HTTP 客户端层 mock,或加载 AuthPlugin —— 它不需要 HTTP 服务器)。ADR-0115 收尾更新里"#4113 是那类假实现仅存的地方"一句已同步收口 —— 这一类到此平台级关闭,不再有编造型 fallback 幸存。
Generated by Claude Code