Skip to content

v2026.09.19.2

Choose a tag to compare

@westsource westsource released this 19 Sep 12:48
· 19 commits to main since this release

v2026.09.19.2 — 服务端加固:发信防滥用 · 登录支持已验证邮箱 · 仅回环监听

服务端专用发布:CLIENT_VERSION 保持 2026.09.13.4 不变 —— 客户端包零改动,/api/client/manifest 不触发自动更新,各端无需重启。本版汇总自 v2026.09.13.4 以来的 14 次服务端发布(2026.09.14.1 → 2026.09.19.2)。无破坏性 schema 变更:新增 mail_stats、announcement_dismissals 两张表与 access_device.user_id 一列,全部由启动时的 init_db() 幂等创建。

Added(发信防滥用:收件人侧限流 + 全站日预算 + 验证码)

  • 收件人侧限流(ratelimit.py 新 scope,key 是收件地址/账号而非来源 IP —— 换 IP 不能放大):mail_recipient 1 封/分钟 + 5 封/天(已解析的已验证邮箱)、mail_account 10 封/小时(bind / resend / 安全中心,按 user_id)、mail_address 1 封/10 分钟(调用方自填地址)。此前唯一上限是每 IP 5 封/10 分钟 = 单邮箱 720 封/天/IP。
  • 全站每日发信预算:HERMES_SYNC_MAIL_DAILY_CAP(默认 200),计数落 mail_stats(sent / rejected / failed),DB 计数所以重启不清零;超限拒绝发送并记 rejected(可作滥用告警信号)。动机:发信账号有服务商日额度,被刷爆后所有用户收不到激活/重置邮件,而 /web/forgot 是静默失败,用户只会觉得"没收到"。
  • /web/forgot 挂自托管验证码(复用 captcha,图形为纯矢量线条、答案只存服务端):闸门在任何账号查询之前,失败只回表单页且保留已填标识符,不泄露标识符是否存在。
  • 登录页明确支持"已验证邮箱":标识符可用用户名或已验证邮箱(用户名优先匹配,大小写不敏感;未验证邮箱不可用),标签与提示随邮件功能开关显示;新增 server/tests/test_login.py 8 例覆盖解析规则与"两种标识符失败响应逐字节相同"。

Changed(发信路径与监听面)

  • 验证码改为纯矢量渲染:原先把算式写成 SVG <text> 节点,一个正则即可读出算式;现在只用 <path>(7 段式折线 + 随机旋转/抖动/线宽)与装饰线点,标记里不含任何字符数据。这是成本门槛,不是人性证明 —— 真正的天花板是上面的收件人桶与日预算。
  • 发信移出事件循环:六个发信点全部改走 asyncio.to_thread(对齐 render / sync 的做法)。此前同步 smtplib(timeout=20、每封新建连接)直接在 async 处理器里调用,单进程 uvicorn 下每个发信请求都会卡住登录与同步。
  • 限流计数表满时改为失败关闭:原先超过 20000 个桶就整表 clear(),等于让刷不同 key 的调用方把所有人的预算(含 login 桶)一起清零;现在先淘汰过期条目,仍满则拒绝该请求并打印告警。
  • 服务端只监听 127.0.0.1:8765(原 0.0.0.0):反向代理成为唯一对外入口,明文端口不再出现在任何非回环接口上;request.client.host 只可能来自代理,按 IP 限流与来源审计的前提由"依赖防火墙/安全组"变为"进程自身保证"。自托管部署必须前置反向代理才能从其他机器访问(README / SECURITY / 部署文档已同步)。
  • 侧边栏「官网」入口 + ?landing=1(已登录用户可显式请求落地页),文案「官网」→「官方首页」;「提交反馈」改为 GitHub Issues 外链,删除站内反馈整套死代码。

Fixed

  • REQ src= 不再取 X-Forwarded-For 最左值(那是调用方可控的):改用 uvicorn 解析出的真实对端 IP,原始链另存 xff= 字段。限流一直是按真实 IP 记的(伪造 XFF 绕不过),改的是审计与取证的可信度。
  • 邮箱注册的首次激活必 500:RealDictCursor 上按位置取 RETURNING id,改为按列名取值。
  • 邮件正文改为可点击链接(multipart/alternative,HTML 里锚文本即完整 URL);链接有效期统一为 3 小时并由常量派生(激活链接从 30 分钟 → 12 小时 → 3 小时,重置保持 30 分钟),邮件文案与等待页文案不再各写一份。
  • 落地页「支持的 Agent」计数不再写死(由 PUBLIC_AGENTS 派生,并补齐枚举漏项)。

Added(运营与可见性)

  • 公告横幅(可选,默认关闭):HERMES_SYNC_ANNOUNCEMENTS_URL 指向公开 feed 时,登录用户看到顶部横幅;服务端不出网,由浏览器直取 feed;announcement_dismissals 表 + POST /web/announcement/dismiss 记录已读。
  • 设备访问归属到用户:access_device.user_id 记录 API key 对应的归属,管理页设备明细可按用户/Agent 展开;回填脚本 scripts/backfill-access-device-user.py。

生产状态:236 已部署(提交 cab5a8a)—— server/ 与 mcp/ 逐文件 sha256 与仓库一致、重启后 systemctl is-active = active、/health 200、journal 无 error;线上实测本次加固项(验证码为矢量且无字符数据、错答被拒、正答走统一响应页、REQ src 为真实对端、mail_stats 计数与超预算拒绝)与登录页新文案(中英)。CLIENT_VERSION 保持 2026.09.13.4,线上 /api/client/manifest 仍服务该版本。

仓库说明:同日为清除历史中的部署细节并统一提交身份为 GitHub noreply,对全部历史做过一次重写(含 tag 重指),历史哈希已变化;旧→新映射保存在本地运维手册(不入库)。