[U-01][high] 开启自助注册时不要求安全码,空安全码即可注册 #39

Closed
opened 2026-09-30 13:57:00 +08:00 by nixevol · 1 comment
Owner

编号:U-01 严重级:high 工作线:身份、群、在线与认证基础(internal/app/identity、group、presence、internal/auth) + 后台网页(web/) 来源:审查 A-02、I-01
依赖:无 被依赖:无

结论与统一方案

两份审查结论一致:新库没有 registration_code 行时,管理员先打开开关、还没设码,register() 读到的存储码是空串,constantTimeEqual("", "") 为真,任何人不带码都能注册;后台 PUT 只传 {"enabled":true} 也会被接受,e2e 主路径正是"先开开关、再存安全码"。

统一方案(后端由身份线完成,界面部分由网页线在本 issue 内完成):

  1. 身份线:register() 读完设置后,存储码不是 8–64 字符就返回 403 registration_closed,记一条不含码的警告日志;放在锁定检查之前,不计入锁定。能修复已处于"开启 + 空码"状态的旧库。
  2. 身份线:internal/admin/registration.go 的 handleRegistrationPut 在同一写操作末尾读回最终的开关与安全码;若最终为开启且安全码不足 8 字符,返回 400"开启自助注册前须先设置 8–64 字符的安全码"并整体回滚;允许一次提交 {"enabled":true,"generate":true} 或 {"enabled":true,"code":"..."}。
  3. 网页线:RegistrationView.vue 在没有已保存的安全码时禁用开关,并直接显示提示;e2e 改为先设码再开启;同步 docs/api/admin-api.md 5.2。

改动文件

internal/app/identity/register.go、internal/admin/registration.go、web/src/views/RegistrationView.vue、web/e2e/admin-main.spec.ts、docs/api/admin-api.md。

与其他问题的交互 / 冲突说明

  • registration.go 在管理后端线没有其他改动(H-02 只在其中加审计 detail,合并时保留)。
  • RegistrationView.vue 由网页线在本 issue 内修改;W-01 改通用错误处理时注意合并。

验收与测试

  • 新库 {"enabled":true} 返回 400,开关仍为关闭;{"enabled":true,"generate":true} 返回 200。
  • enabled=1 而没有码行或码为空串时,空码与任意码都返回 403 registration_closed。

问题明细(各区审查原文,证据含文件与行号)

以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。

[A-02] 注册可以在没设安全码时开启,空安全码即可注册

  • 严重级:high
  • 分类:安全 / 与PRD不符
  • 现象与影响:
    • 新库上 PUT /api/admin/registration {"enabled":true} 会成功。
    • 注册接口拿空安全码比较时 constantTimeEqual("", "") 为真,Validate 也不检查安全码,于是不带安全码就能注册。
    • 后台开关直接发 {enabled:true}。e2e 主路径就是"先打开开关、再填安全码",两步之间注册完全敞开。
    • 注册出的端能列出目录,也能给没设对话密码的端发消息(D26)。
  • 证据:
    • internal/admin/registration.go:47-67 只要求"至少一项",写入 registration_enabled=1 前不检查安全码是否存在。
    • 注册接口的比较逻辑如下;internal/protocol/validate.go:709-726 不校验 registration_code。
	if !constantTimeEqual(req.RegistrationCode, storedCode) {
		h.cfg.Locks.Fail(lockKey)
		return RegisterResult{}, apiErr(http.StatusForbidden, protocol.CodeRegistrationCodeInvalid, "registration code invalid")
	}
func constantTimeEqual(a, b string) bool {
	// 长度不同时 ConstantTimeCompare 直接失败;先按较短侧对齐比较,再核对长度,避免过早返回。
	ab := []byte(a)
	bb := []byte(b)
	if len(ab) != len(bb) {
		dummy := make([]byte, len(ab))
		subtle.ConstantTimeCompare(ab, dummy)
		return false
	}
	return subtle.ConstantTimeCompare(ab, bb) == 1
  • web/src/views/RegistrationView.vue:29-38,74-79 开关直接调 updateRegistration({ enabled })。
  • web/e2e/admin-main.spec.ts:107-113 先点开关,后保存安全码。
  • a3_test.go:102-111 只测了带安全码开启的情况。
  • 文档依据:PRD F23(约 448–449 行)"管理员在后台开启,并设置注册安全码(8–64 字符)""注册必须带当前的注册安全码,错误则失败";D14。
  • 为何不是故意设计:A3、I1 两节只定了开关取值和审计不写明文,没有"允许无码开启"的决定。
  • 解决方案:
    1. A 线:handleRegistrationPut 在同一个写操作末尾读回最终的开关和安全码。若最终为开启且安全码不足 8 字符,返回 400"开启自助注册前须先设置 8–64 字符的安全码",整个写操作回滚。允许一次提交 {"enabled":true,"generate":true} 或 {"enabled":true,"code":"..."}。
    2. I 线纵深防御:存储的安全码为空时按 registration_closed 处理,也能修好已经处于"开启 + 空码"状态的旧库。
    3. 前端:data.code 为空时禁止打开开关,并直接显示 n-alert"请先保存或生成安全码"。
    4. 同步 admin-api.md 5.2,把 e2e 改成先设码再开启。
  • 改动文件:internal/admin/registration.go、internal/app/identity/register.go(I 线)、RegistrationView.vue、web/e2e/admin-main.spec.ts、docs/api/admin-api.md(总控)。
  • 交互/冲突风险:test/accept/admin_helpers.go:58-61 和各 SDK 测试启动器都是 enabled 与 code 一起提交,兼容。只有 e2e 需要调整。
  • 需补测试:
    • 新库 {"enabled":true} 返回 400,且开关仍为关闭。
    • {"enabled":true,"generate":true} 返回 200。
    • identity:开启加空码时,用空安全码注册返回 registration_closed。
  • 置信度:代码阅读确定

[I-01] 开了注册但没设安全码时,空安全码也能注册

  • 严重级:high
  • 分类:安全 / 与PRD不符
  • 现象与影响
    • 新库里没有 registration_code 这一行。管理员先打开开关、还没设码时,register() 读到的存储码是空串。
    • 请求不带码或带空码时,constantTimeEqual("", "") 返回 true。原因是 subtle.ConstantTimeCompare 对两个空切片返回 1。结果是任何人都能注册。
    • 后台页面上开关在最上面,和安全码是两个独立操作。PUT 只传 {"enabled":true} 也会被接受,这种误操作很容易发生。
  • 证据
    • internal/app/identity/register.go:140-156 只判断开关,然后直接比较安全码。
    • register.go:233-244 在没有该行时返回空串。
    • register.go:300-310 是 constantTimeEqual 的实现。
    • internal/admin/registration.go:55-67 只写 enabled;82-95 只在传了 code 时才校验长度。
    • web/src/views/RegistrationView.vue:29-38、74-79:开关单独调用接口。
	if !constantTimeEqual(req.RegistrationCode, storedCode) {
		h.cfg.Locks.Fail(lockKey)
		return RegisterResult{}, apiErr(http.StatusForbidden, protocol.CodeRegistrationCodeInvalid, "registration code invalid")
	}
  • 文档依据:PRD F23(448-449)写的是「管理员在后台开启,并设置注册安全码(8–64 字符)」「注册必须带当前的注册安全码」;另见 D14。
  • 为何不是故意设计:I1 和 A 各节都没有「空码放行」的约定。安全码是注册的唯一门槛。
  • 解决方案
    1. 身份线:register() 读完设置后,如果存储码不是 8–64 字符,就返回 403 registration_closed,并记一条不含码的警告日志。这一步放在锁定检查之前,不计入锁定。
    2. 后台线:handleRegistrationPut 在事务里判断,如果本次结果是开启、但库里没有合法码、本次也没传码或要求生成,就返回 400「请先设置安全码」。
    3. 前端线(可选):没有码时禁用开关。
  • 改动文件:internal/app/identity/register.go、internal/admin/registration.go,可选 web/src/views/RegistrationView.vue。
  • 与其他模块的交互/冲突风险:现有测试开注册时都带了码,不受影响。
  • 需补测试
    • enabled=1,分别在「没有码行」和「码为空串」两种情况下,空码和任意码都返回 403 registration_closed。
    • 后台只传 enabled:true、库里没有码时返回 400。
  • 置信度:代码阅读确定。

复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。

**编号**:U-01 **严重级**:high **工作线**:身份、群、在线与认证基础(internal/app/identity、group、presence、internal/auth) + 后台网页(web/) **来源**:审查 A-02、I-01 **依赖**:无 **被依赖**:无 ### 结论与统一方案 两份审查结论一致:新库没有 `registration_code` 行时,管理员先打开开关、还没设码,`register()` 读到的存储码是空串,`constantTimeEqual("", "")` 为真,任何人不带码都能注册;后台 PUT 只传 `{"enabled":true}` 也会被接受,e2e 主路径正是"先开开关、再存安全码"。 统一方案(后端由身份线完成,界面部分由网页线在本 issue 内完成): 1. **身份线**:`register()` 读完设置后,存储码不是 8–64 字符就返回 403 `registration_closed`,记一条不含码的警告日志;放在锁定检查之前,不计入锁定。能修复已处于"开启 + 空码"状态的旧库。 2. **身份线**:`internal/admin/registration.go` 的 `handleRegistrationPut` 在同一写操作末尾读回最终的开关与安全码;若最终为开启且安全码不足 8 字符,返回 400"开启自助注册前须先设置 8–64 字符的安全码"并整体回滚;允许一次提交 `{"enabled":true,"generate":true}` 或 `{"enabled":true,"code":"..."}`。 3. **网页线**:`RegistrationView.vue` 在没有已保存的安全码时禁用开关,并直接显示提示;e2e 改为先设码再开启;同步 `docs/api/admin-api.md` 5.2。 ### 改动文件 `internal/app/identity/register.go`、`internal/admin/registration.go`、`web/src/views/RegistrationView.vue`、`web/e2e/admin-main.spec.ts`、`docs/api/admin-api.md`。 ### 与其他问题的交互 / 冲突说明 - `registration.go` 在管理后端线没有其他改动(H-02 只在其中加审计 detail,合并时保留)。 - RegistrationView.vue 由网页线在本 issue 内修改;W-01 改通用错误处理时注意合并。 ### 验收与测试 - 新库 `{"enabled":true}` 返回 400,开关仍为关闭;`{"enabled":true,"generate":true}` 返回 200。 - enabled=1 而没有码行或码为空串时,空码与任意码都返回 403 `registration_closed`。 --- ### 问题明细(各区审查原文,证据含文件与行号) > 以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。**解决方案以本 issue 上方的"结论与统一方案"为准**;原文里的方案与之不一致时,按上方执行。 #### [A-02] 注册可以在没设安全码时开启,空安全码即可注册 - 严重级:high - 分类:安全 / 与PRD不符 - 现象与影响: - 新库上 `PUT /api/admin/registration {"enabled":true}` 会成功。 - 注册接口拿空安全码比较时 `constantTimeEqual("", "")` 为真,`Validate` 也不检查安全码,于是不带安全码就能注册。 - 后台开关直接发 `{enabled:true}`。e2e 主路径就是"先打开开关、再填安全码",两步之间注册完全敞开。 - 注册出的端能列出目录,也能给没设对话密码的端发消息(D26)。 - 证据: - `internal/admin/registration.go:47-67` 只要求"至少一项",写入 `registration_enabled=1` 前不检查安全码是否存在。 - 注册接口的比较逻辑如下;`internal/protocol/validate.go:709-726` 不校验 `registration_code`。 ```153:156:e:\code\NixMsg\internal\app\identity\register.go if !constantTimeEqual(req.RegistrationCode, storedCode) { h.cfg.Locks.Fail(lockKey) return RegisterResult{}, apiErr(http.StatusForbidden, protocol.CodeRegistrationCodeInvalid, "registration code invalid") } ``` ```300:309:e:\code\NixMsg\internal\app\identity\register.go func constantTimeEqual(a, b string) bool { // 长度不同时 ConstantTimeCompare 直接失败;先按较短侧对齐比较,再核对长度,避免过早返回。 ab := []byte(a) bb := []byte(b) if len(ab) != len(bb) { dummy := make([]byte, len(ab)) subtle.ConstantTimeCompare(ab, dummy) return false } return subtle.ConstantTimeCompare(ab, bb) == 1 ``` - `web/src/views/RegistrationView.vue:29-38,74-79` 开关直接调 `updateRegistration({ enabled })`。 - `web/e2e/admin-main.spec.ts:107-113` 先点开关,后保存安全码。 - `a3_test.go:102-111` 只测了带安全码开启的情况。 - 文档依据:PRD F23(约 448–449 行)"管理员在后台开启,并设置注册安全码(8–64 字符)""注册必须带当前的注册安全码,错误则失败";D14。 - 为何不是故意设计:A3、I1 两节只定了开关取值和审计不写明文,没有"允许无码开启"的决定。 - 解决方案: 1. A 线:`handleRegistrationPut` 在同一个写操作末尾读回最终的开关和安全码。若最终为开启且安全码不足 8 字符,返回 400"开启自助注册前须先设置 8–64 字符的安全码",整个写操作回滚。允许一次提交 `{"enabled":true,"generate":true}` 或 `{"enabled":true,"code":"..."}`。 2. I 线纵深防御:存储的安全码为空时按 `registration_closed` 处理,也能修好已经处于"开启 + 空码"状态的旧库。 3. 前端:`data.code` 为空时禁止打开开关,并直接显示 `n-alert`"请先保存或生成安全码"。 4. 同步 `admin-api.md` 5.2,把 e2e 改成先设码再开启。 - 改动文件:`internal/admin/registration.go`、`internal/app/identity/register.go`(I 线)、`RegistrationView.vue`、`web/e2e/admin-main.spec.ts`、`docs/api/admin-api.md`(总控)。 - 交互/冲突风险:`test/accept/admin_helpers.go:58-61` 和各 SDK 测试启动器都是 enabled 与 code 一起提交,兼容。只有 e2e 需要调整。 - 需补测试: - 新库 `{"enabled":true}` 返回 400,且开关仍为关闭。 - `{"enabled":true,"generate":true}` 返回 200。 - identity:开启加空码时,用空安全码注册返回 `registration_closed`。 - 置信度:代码阅读确定 #### [I-01] 开了注册但没设安全码时,空安全码也能注册 - **严重级**:high - **分类**:安全 / 与PRD不符 - **现象与影响** - 新库里没有 `registration_code` 这一行。管理员先打开开关、还没设码时,`register()` 读到的存储码是空串。 - 请求不带码或带空码时,`constantTimeEqual("", "")` 返回 true。原因是 `subtle.ConstantTimeCompare` 对两个空切片返回 1。结果是任何人都能注册。 - 后台页面上开关在最上面,和安全码是两个独立操作。PUT 只传 `{"enabled":true}` 也会被接受,这种误操作很容易发生。 - **证据** - `internal/app/identity/register.go:140-156` 只判断开关,然后直接比较安全码。 - `register.go:233-244` 在没有该行时返回空串。 - `register.go:300-310` 是 `constantTimeEqual` 的实现。 - `internal/admin/registration.go:55-67` 只写 enabled;`82-95` 只在传了 code 时才校验长度。 - `web/src/views/RegistrationView.vue:29-38`、`74-79`:开关单独调用接口。 ```153:156:e:\code\NixMsg\internal\app\identity\register.go if !constantTimeEqual(req.RegistrationCode, storedCode) { h.cfg.Locks.Fail(lockKey) return RegisterResult{}, apiErr(http.StatusForbidden, protocol.CodeRegistrationCodeInvalid, "registration code invalid") } ``` - **文档依据**:PRD F23(448-449)写的是「管理员在后台开启,并设置注册安全码(8–64 字符)」「注册必须带当前的注册安全码」;另见 D14。 - **为何不是故意设计**:I1 和 A 各节都没有「空码放行」的约定。安全码是注册的唯一门槛。 - **解决方案** 1. 身份线:`register()` 读完设置后,如果存储码不是 8–64 字符,就返回 403 `registration_closed`,并记一条不含码的警告日志。这一步放在锁定检查之前,不计入锁定。 2. 后台线:`handleRegistrationPut` 在事务里判断,如果本次结果是开启、但库里没有合法码、本次也没传码或要求生成,就返回 400「请先设置安全码」。 3. 前端线(可选):没有码时禁用开关。 - **改动文件**:`internal/app/identity/register.go`、`internal/admin/registration.go`,可选 `web/src/views/RegistrationView.vue`。 - **与其他模块的交互/冲突风险**:现有测试开注册时都带了码,不受影响。 - **需补测试** - enabled=1,分别在「没有码行」和「码为空串」两种情况下,空码和任意码都返回 403 `registration_closed`。 - 后台只传 `enabled:true`、库里没有码时返回 400。 - **置信度**:代码阅读确定。 --- <sub>复审基线:main `4059a15`(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。</sub>
nixevol added the P1-highlane/identitylane/webreview-2026-09-30 labels 2026-09-30 13:57:00 +08:00
Author
Owner

已合入 origin/main 0c9b459。落地提交 8c20b76 fix: 开启自助注册须先有 8-64 字符安全码 (#39)。

已合入 origin/main `0c9b459`。落地提交 `8c20b76` fix: 开启自助注册须先有 8-64 字符安全码 (#39)。
Sign in to join this conversation.