Repository navigation
Description
在 #5712(localization options 表)的核查中发现,记录下来交 triage。不在 #5712 的完成范围内 —— #5712 处理的是 select 的 options 词表,这条是同一个 validatePatch 里另外四个已声明但完全未执行的约束。
事实
packages/spec/src/system/settings-manifest.zod.ts 的 SpecifierSchema 声明了五类值约束,pattern / min / max / minLength / maxLength。
packages/services/service-settings/src/settings-service.ts 的 SettingsService.validatePatch(约 :899-1025)只有三个分支:required、options(#5131)、pattern。min / max / minLength / maxLength 在整个写入路径上没有任何读取点 —— grep 全仓,这四个键在 service-settings 里只出现在 manifest 字面量中,没有消费者。
实测(worktree 内探针,SettingsService + 内存后端):
number quota declared min:0 max:100 — 写 -500,读回 -500;写 999999,读回 999999
text code declared minLength:2 maxLength:4 — 写 "X",读回 "X";写 "ABCDEFGHIJ",读回 "ABCDEFGHIJ"
slider ratio declared min:0 max:1 — 写 42,读回 42
为什么不是休眠项:已上线的 manifest 真的在声明这些边界
不是「没人用的能力」,而是在库的声明与执行不一致,且落点包含安全项:
| 文件:行 | 键 | 声明 | 写入侧实际 |
|---|---|---|---|
packages/services/service-settings/src/manifests/auth.manifest.ts:85-90 |
password_min_length |
min: 6, max: 64 |
接受 1(甚至负数) |
auth.manifest.ts:94-100 |
password_max_length |
min: 16, max: 256 |
接受任意值 |
auth.manifest.ts:128-131 |
password_min_classes |
min: 1, max: 4 |
接受任意值 |
auth.manifest.ts:139-142 |
password_history_count |
min: 0, max: 24 |
接受负数 |
auth.manifest.ts:150-153 |
password_expiry_days |
min: 0, max: 3650 |
接受负数 |
ai.manifest.ts:189/193/197/216/288/294 |
temperature / max_tokens / timeout / 等 6 项 | min/max |
接受任意值 |
即 PUT /api/settings/auth 写 password_min_length: 1 会被接受并生效,而 UI 的数字框声明的下限是 6。攻击面不是「绕过 UI」本身,而是 Prime Directive #10 的正面形状:声明了却没执行,下游(better-auth 口令策略)按写入值工作,没有任何一层把它拉回声明的区间。
与 #5131 的关系(为什么算同一族)
#5131 关掉的正是同一个洞的 options 那一半:在它之前 options 表「只是前端约定」,PUT /api/settings/:ns 什么都收。min/max/minLength/maxLength 今天仍停在 #5131 之前的状态。修法应当与 #5131 同形:在 validatePatch 里补分支,发 out_of_range / too_short / too_long 一类 FieldError(packages/spec/src/api/errors.zod.ts 已有的码里挑,ADR-0114 的 constraint 字段带上 { min, max }),并沿用 #5131 的 TOUCH 闸门语义 —— 只校验本次 patch 触及的键,避免历史漂移把工作区锁死。
需要一并决定的一点:env 侧(#5204)目前只对 options 表做拒收。若补 min/max,应同样走 effectiveEnvOverride 那一个判定点,而不是在 env 路径上开第二份实现(#5204 的成因就是同一比较有两份)。
影响面(已核实)
- 写入侧:活的,任何有
setup.write的调用方(REST / 脚本 / AI 生成的引导代码)都能写越界值。 - env 侧:
OS_AUTH_PASSWORD_MIN_LENGTH=1同样不被拦。 - 未核实:各消费者(better-auth 策略、AI 服务)对越界值的实际容忍度 —— 这决定严重度是「配置面不一致」还是「可利用」,建议 triage 时定。
相关
- service-settings: select 型 specifier 的 options 在保存期完全不校验 —— 声明的枚举不被强制 #5131 —— options 表在写入路径上的执行(同一函数,同一族)。
- env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204 —— env 侧与写入侧对齐 options 表。
- localization 的 timezone / currency 用「策展式 options 表」当穷尽执行边界:合法的 IANA 时区与 ISO 4217 币种被拒 #5712 —— localization options 表的词表问题(发现本条的上下文)。
- feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814 —— sms 配额将新增
daily_quota(0 = 不限),若按min: 0声明,今天同样不会被执行。
Activity
claude commentedon Aug 6, 2026
分诊:
pm:queue+domain:services。packages/services/service-settings/src/settings-service.ts的SettingsService.validatePatch(约 :899-1025)—— 在required/options/pattern三个分支旁补min/max/minLength/maxLength。声明侧packages/spec/src/system/settings-manifest.zod.ts的SpecifierSchema已经声明了这五类约束,本单是执行侧补齐,⛔ 不需要改packages/spec⇒ 不转 spec 座位;若实现中发现必须动SpecifierSchema或errors.zod.ts的码表,STOP 上报走跨座位转移。FieldError(码从packages/spec/src/api/errors.zod.ts现有集合里挑,ADR-0114 的constraint带{min,max})+ 沿用 service-settings: select 型 specifier 的 options 在保存期完全不校验 —— 声明的枚举不被强制 #5131 的 TOUCH 闸门语义(只校验本次 patch 触及的键,避免历史漂移锁死工作区)。无待答的产品问题。effectiveEnvOverride那一个判定点,⛔ 不开第二份实现 —— env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204 的成因就是同一比较有两份。pm:queue常规档,不升 p0 —— 写入需setup.write权限,不是未授权可达;但auth.manifest.ts的六项口令策略键(password_min_length声明min:6却接受1)使其带安全外溢,车道排序时优于纯配置面缺陷。正文「未核实各消费者对越界值的容忍度」不是入队阻塞项,承接方在 PR 里如实记录即可。validatePatch/password_min_length/minLengthsettings 命中 service-settings: select 型 specifier 的 options 在保存期完全不校验 —— 声明的枚举不被强制 #5131(options 半边,已闭)、env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204(env 对齐)、localization 的 timezone / currency 用「策展式 options 表」当穷尽执行边界:合法的 IANA 时区与 ISO 4217 币种被拒 #5712(发现上下文,services 队列)与本单;feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814(smsdaily_quota将声明min:0)是本单落地后的受益方,已在正文点名。无重复入口。git log --oneline origin/main -- packages/services/service-settings/src/settings-service.ts无 service-settings: select 型 specifier 的 options 在保存期完全不校验 —— 声明的枚举不被强制 #5131 之后触及validatePatch的改动,四个键在写入路径上仍零读取点。前提成立。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
hotlong commentedon Aug 7, 2026
认领:PM 循环第 6 轮(rc.5 发布后恢复)
会话:
session_015a5qkLzpGXhLL2F5gvJ7dD分支:
claude/issue-5932-settings-range-enforceWorktree:
objectstack-5932域:
domain:services文件面:
packages/services/service-settings/src/(settings-service.ts 的 validatePatch + env 侧 effectiveEnvOverride 判定点 + 测试)按分诊钉死的修法实施:照 #5131 同形补
min/max/minLength/maxLength四分支 + 既有码表 FieldError(ADR-0114constraint带{min,max})+ TOUCH 闸门语义;env 侧走单判定点不开第二份实现。⛔ 若必须动packages/spec(SpecifierSchema / errors.zod.ts 码表)即 STOP 上报走跨座位转移,不自行实施。Generated by Claude Code
hotlong commentedon Aug 7, 2026
ACCEPT → PR #6201(draft 转 ready 入队中)。
validatePatch补取值窗口分支,发既有码表min_value/max_value/min_length/max_length(ADR-0114 D2,constraint带完整窗口,长度类附actual);TOUCH 闸门继承(收紧窗口不锁死老工作区);env 侧走effectiveEnvOverride单判定点,并把 reporter 泛化成共享句段——两族约束一份去重、一份脱敏,env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204 的「两份实现各自漂移」病根被顺手拆掉。⛔packages/spec零改动(约束早已声明、码表现员够用,STOP 未触发)。auth.password_min_length: 1端到端拒绝 +OS_AUTH_PASSWORD_MIN_LENGTH=1同裁决);逆向验证方向预判命中(仅回退源码 ⇒ 恰好 16 条拒绝 pin 红、6 条宽容 pin 绿);42 个在库声明窗口扫描 0 默认值越窗,无 manifest 因此不可写。type收口——SpecifierSchema的 superRefine 只把 options 绑到三型,窗口键不绑任何型;在服务侧自拟类型清单即「会漂移的第三份清单」,会把本 issue 复制到下一层。service-sms越面说明(认领文件面外,正当):sms-daily-quota.ts注释曾以「settings 写入路径只执行 required / options / pattern —— 声明的 min / max / minLength / maxLength 一条都不校验(auth.password_min_length 可被写成 1) #5932 未执行 ⇒ min:0 惰性」论证消费侧 clamp,本 PR 证伪该句;dev 做纯注释更正(零断言),并写清 clamp 仍然成立的三个理由(非数值形状 / 小数 / TOUCH 前存量行)。step(同族第五键)按观察类立 settings 的step是同族第五个声明但不执行的值约束键 —— 先要一个「它算不算值约束」的判断 #6199 交分诊;「消费者对越界值的容忍度」如实记为未验,不扩范围。Generated by Claude Code
docs(service-settings): re-anchor the dead tracker citations to the c…