Skip to content

v1.0.4-preview.3

Choose a tag to compare

@CSCITech CSCITech released this 10 Jul 18:40
1f59068

Warning

重要安全与稳定性修复

v1.0.4-preview.3 聚焦管理端 fetch、登录态绑定、Redis 配额缓存、验证码消费、代理信任边界、用户余额字段兼容、MySQL 老库注册兼容和用户通知设置的安全边界。受影响部署可能在特定管理操作、OAuth 绑定、密码重置/注册、高并发缓存更新、反向代理配置、用户余额调整或旧库用户注册场景中暴露 SSRF、进程 panic、缓存扣减丢失、验证码复用、IP 限流绕过、余额上限异常、弱邮箱校验或注册失败等风险。

建议升级后重点验证渠道模型拉取、OAuth/第三方账号绑定、密码重置、用户注册、老 MySQL 数据库上的新用户注册、用户通知邮箱保存、管理端用户余额添加/减少/覆盖、充值码兑换并发处理、模型请求限流、站点与品牌顶部导航设置、模型广场自动链路分组展示,以及 Redis 启用时的用户/token 配额缓存更新。

Note

当前 MAX API 分为正式版和 Preview 预览版。Preview 预览版用于提前开放修复和能力,便于在真实环境中验证兼容性、稳定性和安全性;正式版将在对应 Preview 预览版稳定运行后发布。

Release Notes

GitHub 地址

https://github.com/MAX-API-Next/MAX-API/releases/tag/v1.0.4-preview.3

Highlights

  • 加固管理端渠道模型拉取接口,/api/channel/fetch_models 会按系统 fetch setting 执行 SSRF URL 校验,并通过受保护 HTTP client 发送请求。
  • 修复 OAuth、GitHub、Discord、OIDC、WeChat、LinuxDo、Telegram 和邮箱绑定路径中直接 id.(int) 的 panic 风险,兼容 JSON session 后端反序列化出的数值类型。
  • 将 Redis 普通计数和用户/token 配额 hash 增量更新改为 Lua 原子脚本,避免 TTL 检查和增量写入之间的 TOCTOU 窗口。
  • 收紧通知邮箱格式校验,拒绝 a@@b@.、无点域名和带展示名的非纯邮箱输入。
  • 优化充值码兑换的用户级锁生命周期,锁在无引用后会从内存 map 清理,避免长期运行中按 user id 持续增长。
  • 清理 controller/channel.go 中已迁移到 common.* JSON wrapper 的历史 allowlist 条目,保持 jsonwrapcheck 基线与当前代码一致。
  • 将“排行榜”可见性和登录要求设置合并到“站点与品牌 - 顶部导航”,让顶部导航相关入口在同一个设置区维护。
  • 模型广场价格页会展示管理员配置的自动链路分组,以及自动路由别名到真实计费分组的处理顺序。
  • 加固验证码一次性消费语义,密码重置 token 会在校验通过时原子删除,注册成功后也会消费邮箱验证码,避免同一验证码在有效期内重复提交。
  • 收紧默认 trusted proxy 范围,仅默认信任 loopback;真实反向代理地址需要通过 TRUSTED_PROXIES 显式配置,降低伪造 X-Forwarded-For 绕过 IP 限流的风险。
  • 将用户余额、已用额度和邀请额度相关列升级为 bigint,并放宽管理端 quota 覆盖和增加操作的 32-bit 上限,解除约 4295 显示余额的人工操作天花板。
  • 修复 MySQL 老数据库用户注册时命名锁结果扫描可能访问不存在的 null_int64 表,导致注册失败的问题。
  • 补强 MySQL 用户注册命名锁释放校验,RELEASE_LOCK() 返回 0NULL 时会明确报错,不再被误判为释放成功。
  • 修正内存模式下的模型成功率限流,失败请求不再消耗“成功请求”预算。
  • 限制上游错误响应体读取大小,避免异常 upstream 返回超大错误体时占用过多内存。
  • 在 model 层拒绝删除 root 用户,避免未来绕过 controller 角色检查的调用路径误删最高权限账号。

New Features

  • 新增 controller 层 session id 解析 helper,统一处理 intint64float64 和字符串形式的 session user id。
  • 为管理端模型拉取 SSRF 防护新增回归测试,覆盖私网 base_url 被拒绝的场景。
  • 为 session 数值兼容、通知邮箱格式和充值锁清理补充聚焦测试。
  • 新增验证码“验证并删除”原子 helper,供密码重置等一次性凭证场景复用。
  • 新增内存限流器只读检查能力,用于先判断成功率预算、成功后再记录。
  • 新增 sql.NullInt64 底层 SQL 行扫描回归测试,覆盖 MySQL 注册写锁结果使用的 1NULL 返回值。
  • 新增 MySQL 命名锁成功状态判断测试,覆盖 10NULL 三类返回值。
  • 顶部导航设置同屏管理“模型广场”和“排行榜”的展示与登录要求,保存时分别更新 HeaderNavModulesRankingsModule
  • 模型广场分组筛选新增自动链路说明卡片,帮助管理员和用户理解 auto_routes 的真实计费分组链路。
  • 为 trusted proxy 默认值、验证码一次性消费、模型成功率限流、root 用户删除保护和上游错误体截断补充回归测试。

Improvements

  • FetchModels 拼接 /v1/models 前会裁剪 base_url 末尾斜杠,避免生成重复斜杠路径。
  • FetchModels 在读取响应前提前注册 defer response.Body.Close(),错误状态码也会释放响应体。
  • Redis 增量脚本保留原有语义:只有仍存在且带 TTL 的缓存 key 才会更新,并在同一 Redis 脚本中保留剩余 TTL。
  • OAuth 绑定类接口在 session id 缺失或格式异常时返回 API 错误,不再让类型断言 panic 影响进程稳定性。
  • 通知邮箱保存会先裁剪首尾空白,再以标准邮箱地址入库,减少用户输入差异。
  • 密码重置 token 在密码写库前完成消费,即使后续 DB 操作失败,也不会留下可被并发复用的重置凭证。
  • 注册流程在用户创建成功后消费邮箱验证码,保持验证码与实际注册结果一致。
  • 访问旧的 /system-settings/site/rankings 设置路径会自动跳转到 /system-settings/site/header-navigation,避免历史书签进入无效设置页。
  • 顶部导航保存现在并行提交普通导航模块和排行榜模块,并统一走设置页错误处理。
  • 模型广场自动链路展示会过滤空分组、重复分组和当前列表不可见分组,并在没有 auto_routes 配置时继续使用 legacy auto_groups fallback。
  • MySQL 邮箱归一化写锁改为通过 database/sqlRow().Scan 接收 GET_LOCK / RELEASE_LOCK 结果,避免 GORM 将 sql.NullInt64 误当作模型表解析。
  • MySQL 命名锁获取和释放共用同一个成功状态判断,只有返回有效的 1 才视为锁操作成功。
  • 模型成功率限流不再使用临时计数 key 试探容量,避免失败请求污染成功预算。
  • MySQL/PostgreSQL 启动迁移会检查用户余额相关列的现有类型,只在仍为非 bigint 时执行列类型调整;SQLite 保持原有兼容路径。
  • root 用户删除保护下沉到 model.User.Delete / HardDelete,调用方只传 user id 时会先读取现有角色再判断。
  • 上游错误响应超过 1 MiB 时会截断并在错误信息中标记,保留排障上下文同时控制内存占用。

Bug Fixes

  • 修复管理端 FetchModels 接收请求体 base_url 后使用裸 http.Client{} 直接请求,绕过项目 SSRF 防护的问题。
  • 修复 FetchModels 使用直接 json.NewDecoder,以及渠道密钥处理和 Ollama 拉取流中直接 json.Marshal / json.Unmarshal 的项目规范偏离问题。
  • 修复第三方账号绑定路径中 session id 如果由 JSON 后端恢复为 float64 会触发 panic 的问题。
  • 修复邮箱绑定路径同样直接断言 session id,可能在异常 session 类型下 panic 的问题。
  • 修复 Redis RedisIncr 在 TTL 读取后 key 状态变化时可能静默跳过计数更新的问题。
  • 修复用户/token 配额缓存使用的 RedisHIncrBy 存在同类 TTL TOCTOU 竞态的问题。
  • 修复通知邮箱只检查是否包含 @,导致明显非法邮箱也能保存的问题。
  • 修复 topUpLocks 按 user id 缓存锁对象且不清理,长期运行后可能持续增长的问题。
  • 修复密码重置 token 先校验、后删除导致两个并发请求可能同时通过校验并写入不同新密码的问题。
  • 修复注册成功后邮箱验证码仍保留到过期时间,可能被同一邮箱在窗口期内重复提交的问题。
  • 修复“排行榜”设置与顶部导航设置分散在不同站点子页,导致管理员配置顶部导航入口时需要跨区维护的问题。
  • 修复模型广场把缺失的 auto_routes 当成空数组后,旧版 auto_groups 自动分组 fallback 可能不再展示的问题。
  • 修复使用老 MySQL 数据库注册用户时,GORM 扫描 sql.NullInt64 可能生成 null_int64 表查询并返回 Error 1146 (42S02): Table '...null_int64' doesn't exist 的问题。
  • 修复 MySQL RELEASE_LOCK() 返回 0NULL 时仍被当作释放成功的问题,避免锁状态异常被静默吞掉。
  • 修复管理端用户余额在数据库列仍为 INT 时,最终 quota 可能受 2147483647 quota units(约 4295 显示余额)限制的问题。
  • 修复用户余额、已用额度和邀请额度相关字段在 MySQL/PostgreSQL 老库中仍保留 INT 类型,导致高额度用户可能被截断、溢出或写入失败的问题。
  • 修复默认 trusted proxy 包含 RFC-1918 / link-local 私网段时,处在受信任私网来源的请求可伪造 X-Forwarded-For 绕过 IP 限流的问题。
  • 修复内存模式下模型成功率限流把失败请求也计入成功请求预算的问题。
  • 修复上游错误响应体直接 io.ReadAll,异常 upstream 返回超大错误体时可能放大内存占用的问题。
  • 修复 model.Delete / HardDelete 只依赖 controller 层保护,未来直接调用 model 方法时可能误删 root 用户的问题。

Compatibility Notes

  • 管理端模型拉取现在受全局 fetch setting 约束。若部署确实需要从私网地址或非默认端口拉取模型列表,需要在 fetch setting 中显式允许对应私网 IP、IP 白名单或端口。
  • Redis 配额缓存更新仍只作用于带 TTL 的缓存 key;无 TTL 或不存在的 key 继续保持不更新,避免改变缓存写入边界。
  • OAuth/session id 解析现在接受 JSON session 常见的 float64 数值,但仍拒绝空值、零值和无法解析的类型。
  • 通知邮箱现在要求纯邮箱地址;带展示名的 Name <user@example.com> 不再接受,用户应保存 user@example.com
  • 充值码兑换仍保持同一用户并发互斥;锁清理只发生在当前引用全部释放后,不改变并发拒绝语义。
  • 验证码和密码重置 token 现在是一旦成功校验即消费;用户重复提交同一验证码或重复打开同一重置链接会得到无效结果,需要重新发起验证码或重置邮件。
  • MySQL 部署不需要、也不应新增 null_int64 表;该错误来自注册写锁结果扫描方式,升级代码并重启后即可消除对应假表查询。
  • 如果 MySQL 报告命名锁未被当前连接持有或锁状态不可确认,注册写锁释放会返回显式错误;这类情况应按连接或数据库锁状态异常排查,而不是忽略。
  • 默认 TrustedProxies 不再包含私网网段。部署在 Nginx、Cloudflare Tunnel、内网 LB 或容器网络后方时,应通过 TRUSTED_PROXIES 配置真实代理 IP/CIDR,否则 ClientIP() 会回退到直接连接来源。
  • “站点与品牌 - 排行榜”旧 section 会重定向到“站点与品牌 - 顶部导航”;外部脚本或书签若直接打开旧 URL,建议更新到 /system-settings/site/header-navigation。底层 RankingsModule option 仍独立保存。
  • 模型广场只展示当前可见分组内的自动链路;未配置 auto_routes 的部署继续使用旧 auto_groups fallback。
  • 管理端 quota 覆盖仍拒绝负数,quota 增加仍必须为正数;MySQL/PostgreSQL 老库会在启动迁移中把 users.quotausers.used_quotausers.aff_quotausers.aff_history 转为 bigint,不再以 2147483647 quota units 作为业务上限。升级前建议备份数据库,并确认这些列没有自定义类型约束。
  • 内存模式的模型成功率限制现在只统计 HTTP 状态码小于 400 的请求;如果部署依赖失败请求也消耗成功预算,需要改用总请求数限制表达该策略。
  • 上游错误响应体超过 1 MiB 时不会完整保留在错误信息和调试日志中,排查异常 upstream 时可结合上游日志查看完整错误体。
  • root 用户无法通过 model 层软删除或硬删除;需要处理最高权限账号时应先降级或迁移 root 身份。

Verification

  • go test ./controller -run "Test(FetchModelsRejectsPrivateBaseURL|SessionUserID|NormalizeNotificationEmail|TopUpLock|UpdateUserSettingPreservesUnrelatedSettings|AdminReset.*RequiresPaymentCompliance)$"
  • go test ./controller -run TestQuotaBoundsValidation -count=1
  • go test ./model -run "TestScanNullableInt64UsesSQLRow|TestMySQLNamedLockResultSuccessRequiresOne|TestInsertRejectsConcurrentDuplicateEmailMySQL" -v
  • go test ./model -run "Test(UserUpdateDoesNotOverwriteAccountingFields|UserUpdatePersistsZeroValueProfileFields|UserUpdateDoesNotClearEmailFromLoadedPartialUpdate|UpdateUserSettingOnlyUpdatesSetting)$" -count=1
  • go test ./model -run "Test(WaitPendingLogQuotaDataDrainsEnqueuedWork|EnqueueLogQuotaDataAfterShutdownPersistsSynchronously|Record.*QuotaDataAsync)$"
  • go test ./relay/helper -run "TestStreamScanner|TestNewStreamScanner|Test.*Ping"
  • cd web/default && bun run typecheck
  • go test ./common
  • go test ./middleware
  • go test ./service
  • go test ./model
  • go test ./controller
  • go test ./common ./controller ./model ./relay/helper
  • go run ./tools/jsonwrapcheck
  • git diff --check

Full Changelog: v1.0.4-preview.2...v1.0.4-preview.3