Skip to content

env 来源的 settings 值绕过 manifest 的 options 表校验 —— #5094 在写入 API 上堵住的洞,在 OS_* 覆盖这一侧原样敞开 #5204

Description

@os-zhuang

来自 #5152 的实现(PR 见下)。不是 #5152 的搭车,那边只在自己这一个键的消费端补了拒绝逻辑;这里说的是所有 select/radio/multiselect 键共有的通路。

对着 origin/main(e96ad55c0 之后合入 #5152 分支时现查)。

事实

SettingsService 有两条产出「有效值」的路径,只有一条过 options 表:

  1. 写入路径 setMany → validatePatch(settings-service.ts:731 一带)。plugin-email: SendGrid / Amazon SES 设置项同样后端无实现 —— #5087 的同形缺口 #5094/fix(service-settings,plugin-email): 邮件服务商下拉框只列真能投递的值 (#5094) #5133 把 sendgrid/ses 从 mail.provider 的 options 里摘掉之后,补上了这道 invalid_option 校验,理由原话是「PUT /api/settings/:ns 是可授权的公开面,脚本、迁移或 AI 生成的 bootstrap 代码可以往 select 里写任意字符串,存下来、读回来,再让每个消费方各自即兴发挥」。有测试钉住(settings-service.test.ts:431 起)。

  2. env 路径 get()(settings-service.ts:309-329):

    const envName = envKeyOf(namespace, key);
    const envRaw = this.env[envName];
    if (typeof envRaw === 'string') {
      const def = reg.defaults.get(key);
      const value = coerceEnvValue(envRaw, def);
      return { value, source: 'env', locked: true, ... };
    }

    coerceEnvValue 只按默认值的类型做形状转换,不看 spec.options。env 值在 cascade 里优先级最高且 locked: true,所以它就是有效值。

结果:OS_MAIL_PROVIDER=sendgrid 会被原样交给 mail 插件——正是 #5094 刚摘掉的那个值,从另一扇门走回来了。同理 OS_BRANDING_THEME_MODE=drak、OS_STORAGE_PROVIDER=s4、OS_AI_PROVIDER=openal 等等。

为什么算缺陷而不是「运维自己负责」

  • 仓库自己的判断已经写在 plugin-email: SendGrid / Amazon SES 设置项同样后端无实现 —— #5087 的同形缺口 #5094 的测试注释里:options 表是执行面,不是前端约定。那句话对 env 来源同样成立,只是当时没有覆盖到。
  • 失效是静默的:get() 不 warn,/api/settings/:ns 的读取面会把这个非法值当成 source: 'env' 的正常值展示,消费方各自即兴处理(有的 switch 落 default,有的把它当字符串拼进 URL)。
  • 这正是「declared ≠ enforced」的形状:manifest 声明了封闭值域,一条通路不执行它。

可能的形状(供定夺)

  1. get() 的 env 分支也过 options 表:不匹配就按 error 记一行(写清后果与合法值集),并忽略该 env 值回落到 cascade 的下一层。与 membershipPolicy 无法作为平台设置配置,且注册路径与回填路径读的是两个来源 #5152 在消费端采取的姿态一致,但放在唯一正确的地方,一次覆盖所有命名空间。
  2. 启动时一次性校验:registerManifest 时扫描该命名空间所有键的 env 覆盖,非法的直接拒绝启动(对照 OS_TENANCY_POSTURE 「无法识别的值拒绝启动」的先例)。姿态最硬,也最容易把既有部署拦在门外。
  3. 什么都不做,只在文档里写——但这就是 plugin-email: SendGrid / Amazon SES 设置项同样后端无实现 —— #5087 的同形缺口 #5094 明确反对的那种静默。

倾向 1 + 2 的组合:注册时报一次(够响、够早),运行时忽略非法值(够安全)。但「拒绝启动 vs 忽略」是姿态决定,不该由实现者自己定。

已知的一处已缓解

auth.membership_policy(#5152)在 bindAuthSettings() 里自己挡了一道:非法值记 error 并保持当前策略,不静默落回 auto。那是单点补丁,不是这个通路的修复——本 issue 修好后,那段消费端逻辑可以留着当第二道防线,也可以收敛。

Found-during: #5152

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    ContributorAuthor

    分诊:needs-user-decision + domain:services。

    • 落点:packages/services/service-settings/src/settings-service.ts 的 get() env 分支(coerceEnvValue 只按类型转换、不查 spec.options);services/* 按域表归 domain:services。过时前提检查:origin/main 上该文件自立单以来无相关合并,缺口仍在。
    • 为什么进决策箱而不是队列:修法 1(运行时忽略非法 env 值 + error 日志)与修法 2(registerManifest 时非法 env 覆盖直接拒绝启动)之间是运维姿态拍板 —— 方案 2 会把携带非法 OS_* 值的既有部署拦在启动之外,发单人明说「拒绝启动 vs 忽略」不该由实现者自己定。
    • 待裁决问题:采 1+2 组合(发单人倾向,对照 OS_TENANCY_POSTURE 拒启动先例)/仅 1(最温和)/仅 2?裁决落评论后即可转 pm:queue 派发。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    维护者裁决(2026-08-06,经 services 车道 PM 转达):批准「方案 1 为主 + 注册时响亮 error(不拒绝启动)」

    • get() 的 env 分支过 options 表:非法值记 error(写清后果与合法值集)并忽略该 env 值、回落 cascade 下一层;
    • registerManifest 时扫描该命名空间的 env 覆盖,非法者记一次响亮 error —— 不拒绝启动(避免升级陷阱:昨天合法的值不应把老部署整个拦在门外,plugin-email: SendGrid / Amazon SES 设置项同样后端无实现 —— #5087 的同形缺口 #5094 的 sendgrid 摘除正是这种场景);
    • 安全关键命名空间(tenancy 类)如需硬姿态,后续按键位加旗标另议,不做默认。

    摘 needs-user-decision 回 pm:queue,本车道随后认领直派。


    Generated by Claude Code

  3. self-assigned this
    on Aug 6, 2026
  4. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    认领(services 车道 PM 派发,session_01BWS4heBoAitLmzCLhcYdbK)


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions