Skip to content

cloud 测试发现(框架部分):注册接口的邮箱存在性、邮箱验证只能全开或全关、展示型字段原样存储 HTML(细节按披露裁定隐去) #25040

Closed

Description

@baozhoutao

Ruled: 6308462370 · letters 1 B · 2 A · 3 C · 2026-10-21T05:01Z

从 cloud 的测试记录 objectstack-ai/cloud#3125 中拆出,属于框架(auth / platform-objects)的部分。2026-10-20 在 staging(API 18.0.0-next.4)上黑盒测试发现,做不做、怎么做由维护者决定。

披露(triage R553): 第 1、3 项涉及认证与输入安全,按维护者在 #9387 的裁定(5323310017),本卡只写问题存在、所在位置和适用规则。请求形态、返回码、探测字符串已从本卡移除,原文不会在 GitHub 上恢复。⛔ 本卡、后续 PR 和评论都不放复现方法。

1. 注册接口会暴露某个邮箱是否已注册

  • 位置: packages/plugins/plugin-auth/src/auth-manager.ts 的注册路径。同一套系统里,登录和忘记密码已经统一了响应,注册还没有。
  • 测试方建议: 提供防枚举的注册选项。对已存在的邮箱返回与注册成功一致的响应,并给这个邮箱发一封「你已有账号」的邮件。

2. 邮箱验证只能全开或全关

  • 现象: requireEmailVerification 是全局开关。
    • 关掉时,未验证邮箱的账号可以创建 API 密钥、邀请成员。
    • 打开时,部署方必须先接好邮件服务。cloud#2249 就是因为这个,staging 卡在了注册这一步。
  • 测试方建议: 提供一个折中选项。未验证的账号可以正常登录和体验,但敏感操作(创建 API 密钥、邀请成员、更改邮箱等)要求先验证。

3. 展示型字段原样存储 HTML

  • 位置: sys_user.name 一类展示型字段,服务端既不限制标签,也不限制长度。前端已经转义,但这个值会流到邮件、导出、集成等不一定转义的出口。
  • 测试方建议: 加长度上限,服务端剥离或拒绝 HTML 标签。

附:框架已有、但部署方没开的能力(属 cloud 的配置,供参考)

  • security-headers.ts 支持 HSTS,默认关闭(hsts: false)。
  • password_reject_breached(HIBP)默认关闭。可以考虑生产模式下默认开启。

Generated by Claude Code

Activity

  1. changed the title [-]cloud 测试发现:注册接口可判断邮箱是否已注册、邮箱验证只能全开或全关、展示型字段原样存储 HTML[/-] [+]cloud 测试发现(框架部分):注册接口的邮箱存在性、邮箱验证只能全开或全关、展示型字段原样存储 HTML(细节按披露裁定隐去)[/+] on Oct 21, 2026
  2. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    Contributor

    Triage(R553):转入决策箱,并按披露裁定改写本卡。 · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-21T03:54Z


    Generated by Claude Code

  3. added
    area:identityLogin and identity — sign-up, sessions, organization membership, SSO
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Oct 21, 2026
  4. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    Contributor

    总监增量,批 #381 项 3 — 项目总监席,第 45 场续场 3,session_01Lg6GQ4xjqmvEzYv78d6H7i,2026-10-21T04:16Z。卡面是 cloud#3125 拆出的三项测试发现,无选项表、无四棱块,本增量按项补齐;只写本席复核与新增的内容。⛔ 披露纪律(#9387 5323310017):只写位置、规则与抽象触达,不写请求形态、返回码与探测字串。线程读至 6307632431。

    前提复核(origin/main eddffc3390cf)


    第 1 项 · 注册接口暴露邮箱是否已注册

    选项 做什么 客户可感知后果 工作量
    A 维持现状 已存在地址在注册时被明确拒绝(#15587 的设计) 任何人可用注册接口试出某邮箱是否已有账号(open / email_domain 姿态);用户得到清楚的指引 0
    B 通用成功 + 提醒邮件 open 与 email_domain 姿态下,对已存在地址返回与注册成功一致的响应,并给该邮箱发一封「你已有账号,请登录或重置密码」邮件(接 vendor 的 onExistingUserSignUp 或自建模板);邮件通道不可用时退回现状的拒绝,不留黑洞;invite_only 不变 外人试不出邮箱是否注册;已有账号的用户收到一封邮件告知怎么做 plugin-auth 1 个 PR(S–M):分支按姿态与邮件通道分流 + 一个内置模板 + 钉子;REST 门响应改变属协议类
    C 只在 open 做 B email_domain 保持明确拒绝(同事域内枚举价值低) 同 B,范围更小 同 B 少一个姿态

    业务含义直译: A = 前台直接说「这个人在我们这儿有账」;B = 前台一律说「好的,请查收邮件」,邮件里才告诉本人实情;C = 对外人这样,对同事照实说。

    四轴: ① 主流身份服务(Auth0、Cognito、Supabase)默认防枚举:注册已存在地址返回成功形态并发邮件;B 与之一致,A 把「地址存在」当公开事实。② 拉动来自 cloud staging 黑盒测试(cloud#3125),无客户工单。③ B 的失败方向正是 #15587 记录的黑洞——用户以为注册成功却没收到邮件;所以 B 必须以「邮件通道可用」为前提,否则退回拒绝;A 的失败方向是响亮但泄露存在性。④ B 新增一个内置邮件模板与一条分流;A 零增长。

    推荐:B(带「邮件不可用则退回拒绝」的前提)。 置信缺口:vendor 钩子 onExistingUserSignUp 的行为未实测;email_domain 下的枚举价值未评估(若您认为同事域可照实说,选 C)。自检:只看①选 B;②③④ 是否翻转:否(③ 以前提闭合)。

    第 2 项 · 邮箱验证只能全开或全关

    选项 做什么 客户可感知后果 工作量
    A 维持现状(本席荐) 布尔开关 + 姿态强制 + 显式关的启动告警;cloud staging 接邮件通道或显式关 预生产要么接邮件、要么显式关并接受告警;未验证账号的权限面不变 0
    B 新增「验证前受限」中间档 配置三态(如 requireEmailVerification: 'sensitive'):未验证可登录浏览,创建 API 密钥、邀请成员、改邮箱等敏感门要求已验证 部署方不接邮件也能让人先体验;敏感操作前被要求验证 spec 配置键三态(协议类)+ plugin-auth 敏感门清单与统一断言 M;无先例
    C 只补文档 在 audience 文档写明「open 关验证 = 预生产专用」 无行为变化 S

    业务含义直译: A = 大楼要么查证件要么不查;B = 不查证件可以进大堂,进机房要查;C = 门口贴一张说明。

    四轴: ① B 多一个永久的「敏感操作」清单,每新增一道门都要记得分类;A 一条规则。② 拉动是 staging 的部署便利(cloud#2249),不是客户需求;生产 open 姿态本就强制开。③ B 的失败方向是某个敏感门漏判、未验证账号静默做了敏感操作;A 的失败方向是部署卡在注册步(响亮)。④ B 新增一组门与一个三态键;A 零增长。

    推荐:A,不做。 置信缺口:无。自检:只看①选 A;②③④ 是否翻转:否。

    第 3 项 · 展示型字段原样存储 HTML

    选项 做什么 客户可感知后果 工作量
    A 维持现状 存原样,前端转义,其余出口各自负责 名字里的标签流到邮件、导出、集成时,是否被转义看各出口 0
    B 服务端剥离或拒绝标签 对展示型文本字段(或全部 text)在写入面剥离/拒绝 HTML 标签 含 <、> 的合法文本被改写或拒绝(如「A<B」「Smith & Sons」);需要先定义哪些字段算「展示型」 spec 新键或全局写入规则 + 写入面 M;BREAKING 收窄;无先例
    C 出口按上下文转义(本席荐) 凡把文本字段渲染进 HTML 的出口——邮件模板引擎默认转义、HTML 导出、集成 payload——按上下文转义,并对出口清单加钉子;同时实测 maxLength: 255 是否在写入面强制,未强制则按 #11875 补齐 用户名可以含任何字符;任何 HTML 出口都安全 plugin-email 模板引擎默认转义 + 出口清单审计 M;长度强制 S

    业务含义直译: A = 原件随便写,复印时各部门自己打码;B = 收件时就把所有尖括号剪掉;C = 复印机出厂默认打码。

    四轴: ① 主流做法是存原文、出口转义(Salesforce、Rails、React 的默认);剥离标签有损且可绕过;C 是结构性正确形态。② 拉动是 staging 黑盒(cloud#3125);邮件出口是否真未转义未测(置信缺口)。③ B 的失败方向是误拒合法输入(响亮但错);C 的失败方向是漏一个出口(静默),用「模板引擎默认转义」把它变成结构性而不靠逐处记得。④ B 新增一条全局写入规则;C 不加规则,改默认值。

    推荐:C,附长度强制实测。 置信缺口:邮件模板引擎当前是否转义变量未读;出口清单未穷举。自检:只看①选 C;②③④ 是否翻转:否。

    裁后执行(三项皆按推荐时): 分诊按裁定拆成每项一张执行卡(本卡 R553 评论已写明):第 1 项 plugin-auth(domain:services,REST 响应变化按协议类走达档复核,Clause-②: yes),第 2 项不立卡,第 3 项 plugin-email 出口转义 + 长度强制(domain:services)。附注两项(HSTS、HIBP 默认值)属 cloud 配置,不在本卡。

    os-decision-facets

  5. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #381 item 3 · letters 1 B · 2 A · 3 C · maintainer 「其他同意」 2026-10-21T05:01Z

    Director seat, summon #45 continuation 3, session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #381 item 3 (three items, one letter each), from this card's body, the triage R553 comment 6307632431 and the director's delta 6307902116 (which supplied the options and the four facets per item; its premises were re-read on origin/main eddffc3390cf). The maintainer's 「其他同意」 takes the recommendation on all three. Thread read through 6307902116 at this write; nothing newer. ⛔ Disclosure (#9387, 5323310017): location, rule and abstract reach only.

    Governing text: #15587 (the sign-up path's deliberate refusal of an existing address, packages/plugins/plugin-auth/src/auth-manager.ts:2733–2787, and the black hole it records); #20389 (resolveEmailVerificationRequirement, audience-posture.ts:188–195); #11875 (the write seam and maxLength, spec/src/data/field.zod.ts:131–144); ADR-0080 / ADR-0081 (tag whitelists govern UI pages, not text fields).

    What the ruling binds

    1. Item 1 — B. Under the open and email_domain postures, a sign-up for an address that already has an account answers the same shape as a successful sign-up, and that address receives a mail saying it already has an account (sign in or reset the password) — the vendor's onExistingUserSignUp hook or a built-in template. Precondition, part of the rule: when the mail transport is not live, the path keeps today's explicit refusal, so the plugin-auth: a sign-up for an address that already has a sys_user row answers 200 and persists nothing (posture email_domain) #15587 black hole (a fake success with no mail) cannot recur. invite_only is unchanged (a stranger still learns nothing). The REST door's answer changes: protocol class, contract review at tier.
    2. Item 2 — A. No middle verification tier. The boolean stays with its posture rules (email_domain forced on, open on unless the deployment declares it off, invite_only default off) and its one-line boot warning. cloud staging's registration stop is a deployment configuration (connect a mail transport, or declare verification off), not a framework change.
    3. Item 3 — C. No server-side stripping or refusal of markup in text fields. Every outlet that renders a text field into HTML escapes by context — the mail template engine by default, HTML exports, integration payloads — with a pin on the outlet list. In the same work, the seat measures whether sys_user.name's declared maxLength: 255 (platform-objects/src/identity/sys-user.object.ts:776–781) is enforced at the write seam and, if it is not, brings it under signature / qrcode have no maxLength enforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875's rule.
    4. The appendix items (HSTS default, password_reject_breached default) are cloud configuration and are not this card's.

    Workload of the chosen letters (the delta's columns): item 1 plugin-auth 1 PR (S–M, protocol class); item 2 none; item 3 plugin-email default escaping + outlet audit M, length enforcement S.

    State: needs-user-decision → pm:queue + pm:retriage (held out of dispatch): triage splits this card by its own R553 note — one execution card for item 1 (domain:services, plugin-auth, Clause-②: yes, the mail-transport precondition written into the card), one for item 3 (domain:services, plugin-email outlets + length seam), none for item 2 — each under the same disclosure discipline, then closes this card as the coordination node.

  6. added
    pm:queueReady for the PM dispatch loop
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Oct 21, 2026
  7. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    Contributor

    Split per the ruling, and closed as the coordination node. Triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-21T05:56Z

    The ruling's state line (6308462370) asked for this. Execution:

    Both cards carry the disclosure discipline (#9387).


    Generated by Claude Code

  8. removed
    pm:queueReady for the PM dispatch loop
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:identityLogin and identity — sign-up, sessions, organization membership, SSOdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p2Medium: important, M3security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions