Repository navigation
v1.2.0
🚀 MisMiss v1.2.0 — 安全加固 · 插件副本隔离修复 · 插件定时消息体系
面板鉴权收紧 · 插件消息与普通消息分类 · 账户副本真正生效 · 插件生命周期补齐
v1.2.0 是一次以修缺陷 + 补能力为主的版本:
- 收紧了面板的四处认证绕过(其中三处可直接导致 RCE / SSRF / 信息泄露)
- 修复了 v1.1.0 引入的账户插件副本从未被加载——「副本隔离」此前只对配置/权限/数据成立,对代码不成立
- 建立了插件定时消息体系:与面板添加的普通消息分类管理,不落盘、不可改删、轮转置顶
- 补齐插件生命周期:
on_livestream_bound钩子,插件不再需要靠弹幕事件兜底重试
🔒 安全修复
面板中间件此前把若干前缀整段设为公开,导致以下端点无需登录即可调用:
| 端点 | 风险 | 修复 |
|---|---|---|
POST /api/config/pip-install |
任意 pip install → 任意代码执行 |
要求管理员登录;拒绝 - 开头的包名(阻断 --target / -r 参数注入),限长并加 --no-input |
POST /api/plugin/install* |
上传 zip + 裸 extractall → Zip Slip |
要求管理员登录;逐成员校验路径,拒绝绝对路径 / .. 段 / 盘符 |
GET /api/proxy/image |
无限制 → SSRF / 开放代理 | 域名白名单(可在 config.yml 的 proxy.allowed_hosts 扩展)+ 内网地址拦截 + 重定向逐跳校验 + 10MB 上限 |
WS /api/ws |
无鉴权 → 日志泄露 | 要求管理员 token(浏览器 WebSocket 无法设置 header,故走查询参数);鉴权失败以关闭码 4401 断开,前端据此停止重连 |
- 图片代理保持匿名可访问(前端
<img>无法携带 Authorization header),改以「白名单 + 内网拦截」限制目标 - token 现在来自查询参数(外部可控),
_load_token增加 64 位十六进制格式校验,堵住文件路径穿越
✨ 新增功能
插件定时消息体系
插件注册的定时消息与面板添加的消息分类管理:
| 维度 | 普通消息 | 插件消息 |
|---|---|---|
| 注册途径 | 面板「定时消息」页 | Plugin.register_timer_message() |
| 持久化 | 写入账户 state 文件 | 不写入 |
| 编辑 / 删除 / 排序 | 允许 | 禁止(但可「跳过」「立即发送」) |
| 轮转位置 | 队列中段 | 置顶 |
| 执行指针 | 与插件消息共用同一个 | 同左 |
| 开播条件 | 无 | 可声明 only_when_live=True |
| 生命周期 | 由面板管理 | 插件停用 / 挂起 / 卸载 / 重载时由框架自动清理 |
- 插件消息不落盘,因此插件即使异常终止(
terminate未执行)后重新启用,也不会累积出重复消息 - 框架在插件激活前与停用/挂起/卸载时按插件名强制回收,不依赖插件自觉
- 插件消息增删时自动补偿轮转指针,始终指向原来那条消息
插件生命周期:on_livestream_bound
Plugin 新增 on_livestream_bound(livestream) 钩子,在账户新绑定直播间时触发;
插件被启用/重新启用时若账户已绑定,框架会补发一次。
两条路径都覆盖,插件不再需要靠弹幕事件兜底重试注册。
面板
- 统计区分:账户总览卡片与账户门户概览分别展示「普通 N · 插件 M」
- 来源筛选:定时消息页可按 全部 / 普通 / 插件 筛选(筛选只影响展示,指针位置按后端索引计算,不会错位)
- 插件消息在列表中带「插件」徽标,编辑 / 删除 / 排序按钮置灰
🐛 Bug 修复
- 账户插件副本从未被加载(关键):模块以
import plugins.<dir>.<mod>导入,而账户目录下的
plugins/是插件数据目录,plugins又是命名空间包——仓库的plugins/被合并进来并抢先命中,
于是账户永远执行插件库的源码,installed_plugins/下的副本形同虚设,面板「可更新」徽标也失去意义。
改为按副本绝对路径加载并取唯一模块名,磁盘布局无需迁移 - 插件异常终止后重复注册定时消息:插件消息此前会随 state 落盘,崩溃后残留 + 重新启用再注册 = 重复条目
Server.enable_plugin复刻了一份PluginManager的分支逻辑,导致启用前清理与绑定通知都被跳过
(表现为禁用后再启用,插件定时消息不再恢复)。改为委托,消除重复实现- 合并轮转缓存失效不完整:
move_timer_message是原地交换源列表,缓存持有的是拼接副本不会同步更新 _install_lock形同虚设:在StreamingResponse返回前即释放,并发安装互斥失效;改为在生成器内部持锁,
并给/install、/install/update一并加上- 嵌套结构的 zip 安装报 500:版本比较按固定根路径读
metadata.yaml,而插件名是递归查找的 - 插件元数据缺
name时返回字符串'None'(真值)导致后续判断失真 - 无 token 时前端发出字面量
"Bearer null" - CHANGELOG 存在性判断错误(3 处):其中一处按
module_path字符串反推目录,依赖旧的模块命名格式;
改为在PluginMetadata上记录changelog_path(与既有readme_path同款) - 日志环形缓冲区容量与文档不符(实例化为 2000,类默认值与文档均为 10000),统一为 10000
🔧 改进优化
- WebSocket 重连改为真正的指数退避(2/4/8/16/32s)——此前实现为线性,与文档描述不符
- 合并轮转缓存:
_combined_cycle结果按直播间缓存,避免每次调用新建拼接列表 - 术语统一:「人工消息」全面更名为「普通消息」(代码 / 文档 / 测试)
- 死代码清理:3 个无引用的前端组件(
InstallModal/RoomSelect/ReadmeModal)、重复定义的路由函数、
3 个无用 schema、InlineLoader、TYPE_CHECKING空块、_delete_token、残留的dashboard.pyc - 插件编写约束放宽:各账户插件模块现为独立加载,模块级变量不再跨账户共享;
原「禁止模块级可写状态」的硬约束改为建议(同一账户内重载仍会重新执行模块代码)
📝 文档
README.md— 按多账户模型重写:架构图、事件树(补LiveQuestionEvent/LiveStatisticsEvent)、
插件库与账户副本、_ui_schema.json、Docker 部署包与在线更新、实际项目结构docs/TASKS.md— 校正状态(Question 实体、测试体系等已完成;路径统一为src/core/...)docs/web/README.md— 按实际路由 / 页面 / API 重写,补充认证与授权模型docs/interfaces/interface.md— 定时消息语义更新为三类队列 + 两类消息docs/plugin/PLUGIN_DEV_GUIDE.md— 定时消息章节重写(on_livestream_bound时机、only_when_live)、
生命周期钩子表补齐、副本隔离约束修正- 5 个插件的
metadata.yaml与 3 个插件 README 去除已废弃能力的描述
🧪 测试
新增两组回归测试(与既有测试脚本一样保留在本地开发目录,不随仓库发布):
- 插件定时消息:不落盘、置顶、不可改删移动、按插件清理互不影响、
指针共用与增删补偿、基类接口、账户未绑定直播间返回空串、
异常终止后重新启用不产生重复、仅开播时发送条件、计数区分 - 插件副本隔离:加载账户副本而非插件库源码、副本内相对导入可用、
各账户独立模块对象、卸载后清理模块缓存
另以 HTTP 端到端复核:未登录访问敏感端点被拒、图片代理拦截内网与非法域名、
插件消息的编辑/删除/移动返回 400 而跳过返回 200、账户插件安装与更新全程加载副本。
既有回归(账户管理 / API 集成 / 授权码 / 跨 worker 状态同步 / 定时持久化)全部保持通过。
⚠️ 升级注意
- 插件副本开始生效:此前账户实际执行的是插件库源码,升级后会转而执行
data/accounts/{id}/installed_plugins/下的副本。若某账户的副本比插件库旧,
该账户行为会回退到旧版本——请在账户插件页逐个点「更新」(卡片上有「可更新」徽标) - 日志 WebSocket 需要 token:未登录时不再推送,前端会显示「未授权」并停止重连
- pip 安装与插件上传需要登录(面板用户无需额外操作)
- 插件定时消息不再写入
server_state.json,历史遗留的插件消息条目在下次保存时自然消失
MisMiss v1.2.0 — 门关严了,副本也真跑起来了 🎧