From 59c00271225458e6600fe7fc39a87aa04166428d Mon Sep 17 00:00:00 2001 From: "3371392206@qq.com" <3371392206@qq.com> Date: Sat, 5 Sep 2026 11:42:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(cors):=20SettingsCORSRequest.Origins=20?= =?UTF-8?q?=E6=94=B9=20*string,=E6=94=AF=E6=8C=81=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E6=B8=85=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 2 报告: controller/settings.go:330-332 的死代码 if body.AllowAll == nil && trimAll(body.Origins) == "" && body.Origins != "" { // 空 body 不算错误 } 该条件永远 false (trim 空 != "" 矛盾), 整个 if 块只注释不执行。 更严重的是: 用户无法清空 origins — 原代码 body.Origins == "" 就 400, 但用户可能就是想传空串清空。 struct 字段是 string, 区分不出"未传"和"传空串"。 修法: - struct.SettingsCORSRequest.Origins: string → *string - nil = 未传 (不动) - *"" = 传空串 (清空) - *"url1\nurl2" = 覆盖 - 删掉 line 330-332 死代码 - 改校验逻辑: - 2 个字段都 nil → 400 no_fields - 至少一个非 nil → 200 - 空串不校验 origin 格式 (无 origin 可校), 直接存 行为变更: - {"origins": "https://a.com"} → 写, 同以前 - {"origins": ""} → 清空 (新功能, 以前返 400) - {"origins": null} → 视为未传, 不动 - {"allow_all": true} → 写, 同以前 - {"allow_all": false} → 设为 false, 同以前 - {} → 400 no_fields - {"origins": "not-a-url"} → 400 origin_invalid (同以前) 测试: test/integration/settings_test.go (gitignored) 5 个 case - EmptyOrigins_ClearsOrigins - OriginsNotInBody_NotTouched - EmptyBody_400 - NullOrigins_TreatedAsMissing - InvalidOrigin_400 e2e: 4 场景全过 (设/清空/空 body 400/null 不动)。 不向后兼容: SettingsCORSRequest 内部 DTO, 仅 admin webui 调, 不暴露给外部 OpenAI 客户端。 未 push (待用户) --- controller/settings.go | 51 +++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/controller/settings.go b/controller/settings.go index 9fcee12..9d7e832 100644 --- a/controller/settings.go +++ b/controller/settings.go @@ -299,12 +299,13 @@ func SettingsAuthKeyHandler(w http.ResponseWriter, r *http.Request) { } // SettingsCORSRequest 是 PUT /api/settings/cors 的 body。 -// 两个字段都可选(至少给一个): -// - allow_all: true → 任意 Origin 都接受(*);设了之后 origins 失效 -// - origins: 一行一个 origin,后端 trim + lower + 去末尾 / +// 两个字段都可选(至少给一个),用指针区分"未传"和"传空串": +// - allow_all 指针: nil=未传(不动) *true=开 *false=关 +// - origins 字符串: nil=未传(不动) ""=传空串(清空) "url1\nurl2"=覆盖 +// 这样用户能精确表达意图(保留 / 改 / 清空),不会被 0/"" 歧义坑死。 type SettingsCORSRequest struct { - AllowAll *bool `json:"allow_all,omitempty"` - Origins string `json:"origins,omitempty"` // 也接受 string 数组(任一形式) + AllowAll *bool `json:"allow_all,omitempty"` + Origins *string `json:"origins,omitempty"` // *string 区分"未传(nil)"和"传空串" } // SettingsCORSHandler PUT /api/settings/cors @@ -327,10 +328,9 @@ func SettingsCORSHandler(w http.ResponseWriter, r *http.Request) { middleware.SendJSONError(w, http.StatusBadRequest, "invalid JSON body", "invalid_request_error", "bad_request") return } - if body.AllowAll == nil && trimAll(body.Origins) == "" && body.Origins != "" { - // 空 body 不算错误,用户可能是想"清空"(只清 origins 保留现状) - } - if body.AllowAll == nil && body.Origins == "" { + // 至少要给一个字段(allow_all 或 origins) + // 指针为 nil 表示"未传",不计入 + if body.AllowAll == nil && body.Origins == nil { middleware.SendJSONError(w, http.StatusBadRequest, "at least one of allow_all / origins required", "invalid_request_error", "no_fields") @@ -341,22 +341,27 @@ func SettingsCORSHandler(w http.ResponseWriter, r *http.Request) { if body.AllowAll != nil { updates["cors_allow_all"] = boolToStr(*body.AllowAll) } - if body.Origins != "" { - // 校验每个 origin 至少像 http(s)://... (防止用户填空或填乱字符) - for _, line := range strings.Split(body.Origins, "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - low := strings.ToLower(line) - if !strings.HasPrefix(low, "http://") && !strings.HasPrefix(low, "https://") { - middleware.SendJSONError(w, http.StatusBadRequest, - fmt.Sprintf("invalid origin: %q (must start with http:// or https://)", line), - "invalid_request_error", "origin_invalid") - return + if body.Origins != nil { + // *Origins == "" 表示用户要清空(保留 nil 表示"不动") + origins := *body.Origins + if origins != "" { + // 校验每个 origin 至少像 http(s)://... (防止用户填乱字符) + for _, line := range strings.Split(origins, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + low := strings.ToLower(line) + if !strings.HasPrefix(low, "http://") && !strings.HasPrefix(low, "https://") { + middleware.SendJSONError(w, http.StatusBadRequest, + fmt.Sprintf("invalid origin: %q (must start with http:// or https://)", line), + "invalid_request_error", "origin_invalid") + return + } } } - updates["cors_origins"] = body.Origins + // 空串也能存(表示"清空");trim/lower 在 LoadRuntimeConfig 那侧做 + updates["cors_origins"] = origins } if err := s.SettingsSetBatch(updates); err != nil { log.Printf("[settings] cors set: %v", err)