编号:U-01 严重级:high 工作线:身份、群、在线与认证基础(internal/app/identity、group、presence、internal/auth) + 后台网页(web/) 来源:审查 A-02、I-01 依赖:无 被依赖:无
两份审查结论一致:新库没有 registration_code 行时,管理员先打开开关、还没设码,register() 读到的存储码是空串,constantTimeEqual("", "") 为真,任何人不带码都能注册;后台 PUT 只传 {"enabled":true} 也会被接受,e2e 主路径正是"先开开关、再存安全码"。
registration_code
register()
constantTimeEqual("", "")
{"enabled":true}
统一方案(后端由身份线完成,界面部分由网页线在本 issue 内完成):
registration_closed
internal/admin/registration.go
handleRegistrationPut
{"enabled":true,"generate":true}
{"enabled":true,"code":"..."}
RegistrationView.vue
docs/api/admin-api.md
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。
internal/app/identity/register.go
web/src/views/RegistrationView.vue
web/e2e/admin-main.spec.ts
registration.go
以下是本次复审各区审查报告的原文段落。A、M、I、P、S 开头的是原始发现编号(A 管理后台与网页、M 消息核心、I 身份认证群在线、P 传输平台部署、S SDK)。解决方案以本 issue 上方的"结论与统一方案"为准;原文里的方案与之不一致时,按上方执行。
PUT /api/admin/registration {"enabled":true}
Validate
{enabled:true}
internal/admin/registration.go:47-67
registration_enabled=1
internal/protocol/validate.go:709-726
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
data.code
n-alert
admin-api.md
test/accept/admin_helpers.go:58-61
subtle.ConstantTimeCompare
internal/app/identity/register.go:140-156
register.go:233-244
register.go:300-310
constantTimeEqual
internal/admin/registration.go:55-67
82-95
web/src/views/RegistrationView.vue:29-38
74-79
enabled:true
复审基线:main 4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。
4059a15
已合入 origin/main 0c9b459。落地提交 8c20b76 fix: 开启自助注册须先有 8-64 字符安全码 (#39)。
0c9b459
8c20b76
No dependencies set.
The note is not visible to the blocked user.
编号:U-01 严重级:high 工作线:身份、群、在线与认证基础(internal/app/identity、group、presence、internal/auth) + 后台网页(web/) 来源:审查 A-02、I-01
依赖:无 被依赖:无
结论与统一方案
两份审查结论一致:新库没有
registration_code行时,管理员先打开开关、还没设码,register()读到的存储码是空串,constantTimeEqual("", "")为真,任何人不带码都能注册;后台 PUT 只传{"enabled":true}也会被接受,e2e 主路径正是"先开开关、再存安全码"。统一方案(后端由身份线完成,界面部分由网页线在本 issue 内完成):
register()读完设置后,存储码不是 8–64 字符就返回 403registration_closed,记一条不含码的警告日志;放在锁定检查之前,不计入锁定。能修复已处于"开启 + 空码"状态的旧库。internal/admin/registration.go的handleRegistrationPut在同一写操作末尾读回最终的开关与安全码;若最终为开启且安全码不足 8 字符,返回 400"开启自助注册前须先设置 8–64 字符的安全码"并整体回滚;允许一次提交{"enabled":true,"generate":true}或{"enabled":true,"code":"..."}。RegistrationView.vue在没有已保存的安全码时禁用开关,并直接显示提示;e2e 改为先设码再开启;同步docs/api/admin-api.md5.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,合并时保留)。验收与测试
{"enabled":true}返回 400,开关仍为关闭;{"enabled":true,"generate":true}返回 200。registration_closed。问题明细(各区审查原文,证据含文件与行号)
[A-02] 注册可以在没设安全码时开启,空安全码即可注册
PUT /api/admin/registration {"enabled":true}会成功。constantTimeEqual("", "")为真,Validate也不检查安全码,于是不带安全码就能注册。{enabled:true}。e2e 主路径就是"先打开开关、再填安全码",两步之间注册完全敞开。internal/admin/registration.go:47-67只要求"至少一项",写入registration_enabled=1前不检查安全码是否存在。internal/protocol/validate.go:709-726不校验registration_code。web/src/views/RegistrationView.vue:29-38,74-79开关直接调updateRegistration({ enabled })。web/e2e/admin-main.spec.ts:107-113先点开关,后保存安全码。a3_test.go:102-111只测了带安全码开启的情况。handleRegistrationPut在同一个写操作末尾读回最终的开关和安全码。若最终为开启且安全码不足 8 字符,返回 400"开启自助注册前须先设置 8–64 字符的安全码",整个写操作回滚。允许一次提交{"enabled":true,"generate":true}或{"enabled":true,"code":"..."}。registration_closed处理,也能修好已经处于"开启 + 空码"状态的旧库。data.code为空时禁止打开开关,并直接显示n-alert"请先保存或生成安全码"。admin-api.md5.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。registration_closed。[I-01] 开了注册但没设安全码时,空安全码也能注册
registration_code这一行。管理员先打开开关、还没设码时,register()读到的存储码是空串。constantTimeEqual("", "")返回 true。原因是subtle.ConstantTimeCompare对两个空切片返回 1。结果是任何人都能注册。{"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:开关单独调用接口。register()读完设置后,如果存储码不是 8–64 字符,就返回 403registration_closed,并记一条不含码的警告日志。这一步放在锁定检查之前,不计入锁定。handleRegistrationPut在事务里判断,如果本次结果是开启、但库里没有合法码、本次也没传码或要求生成,就返回 400「请先设置安全码」。internal/app/identity/register.go、internal/admin/registration.go,可选web/src/views/RegistrationView.vue。registration_closed。enabled:true、库里没有码时返回 400。复审基线:main
4059a15(2026-09-30)。编号说明、各工作线的合并顺序、共享文件归属见总览 #7。已合入 origin/main
0c9b459。落地提交8c20b76fix: 开启自助注册须先有 8-64 字符安全码 (#39)。