fix: 修复审计发现的崩溃、权限、CSRF/SSRF 与配额问题 - #4
Merged
Merged
Conversation
## 崩溃类
**_tickTps 引用未定义的 props**(instance.js)—— 该方法里根本没有 `props` 变量
(同名局部变量在另一个方法里),开了 RCON 的实例每 10 秒抛一次 ReferenceError。
而指标循环的回调没有 try/catch、进程也没有 uncaughtException 兜底,于是
**面板进程直接退出**;PM2 拉起后 resumeInstances 又把服务端重启一遍,10 秒后再崩。
触发条件恰好是面板自己推荐的配置(一键开 RCON + TPS 监控要求开 RCON)。
改用 this.getProp(),并补上两道纵深防御:指标循环 per-instance try/catch、
server.js 加全局 uncaughtException/unhandledRejection 兜底。
**tar spawn 缺 error 监听**(backups.js 的 createBackup/restoreBackup)——
同文件的 inspectBackup 是有的。spawn 失败只触发 error 不触发 exit,不挂就是
uncaughtException,而且 Promise 永不 settle → archiveBusy 锁永久泄漏,
该实例之后所有压缩操作恒 409。
**restart 定时器不可取消**(instance.js)—— 手动重启那个 1 秒 setTimeout 没存
handle,cancelAutoRestart 只清崩溃定时器。重启后 1 秒内删实例:目录被 rm,
定时器照常触发 start(),_appendLogFile 的 mkdirSync 又把目录建回来。
## 协作者实时流失效
resolveAllowed 原样展开 collaborators,而 bus 用 includes(username) 做字符串比对 ——
PUT /collaborators 一律写成 {name,role} 对象,所以**通过当前 UI 添加的协作者收不到
任何 log/state/metrics/players 事件**。REST 正常、SSE 静默失效,极易误判成前端问题。
改走已有的 collaboratorList() 归一化。
## 权限分档
- OPERATOR_WRITES 的启停正则是 /^\/(start|…)$/,而真实路由是 /:iid/server/:action,
req.path 为 /server/start —— 永不匹配,启停被判成 manager。方向上是失效关闭
(不越权),但 operator 档因此形同虚设:想给人启停权只能给 manager,而 manager
能改文件改配置,净效果是权限被迫放大。
- viewer 能读 /rcon(明文密码)、/properties(整个 server.properties)、
/files/content、/files/download、/backups/:id/download —— "只读"档能把实例的
全部秘密拿走。新增 MANAGER_READS 把这几条 GET 提到 manager。
- clone 缺 isOwnerOrAdmin:协作者能把别人的实例连同世界和密钥复制成自己完全
掌控的实例,绕过"协作者不能把实例拿走"的边界。
## CSRF / SSRF
- 全站无 CSRF 防护,只有 SameSite=Lax。新增 Origin/Sec-Fetch-Site 校验中间件,
放行 Bearer Token(浏览器不会自动附带,跨源加头会触发预检)与无头的非浏览器
客户端,不误伤脚本。
- webhook / Discord / S3 / WebDAV / OAuth 端点只校验协议不校验地址,而"测试推送"
会回显每个通道的错误 —— 这是个带回显的内网探测原语。新增 utils.checkOutboundUrl,
拒绝环回/私有/链路本地(含 169.254.169.254 云元数据)/CGNAT/组播,IPv4-mapped
IPv6 按内嵌 v4 判定,并对 webhook 关掉重定向跟随。
## 配额
- **恢复备份完全不校验磁盘配额** —— 五处校验之外的第六条写入路径。
inspectBackup 改用 tar -tzvf 拿到未压缩体积,按净增长(归档体积 - 当前占用)校验,
增量链按各归档之和取上界。
- Modrinth 安装预检是写死的 64MB,而下载是无界流式写入。改用 Modrinth 自己给的
file.size 预检,并在下载过程中按已写字节持续复查(声明体积可能是假的)。
## 其余
- notify 的 webhook/Discord URL 与 telegramToken 明文回显(backupRemote 早已掩码,
同一响应里一半掩一半不掩)。补 maskNotify 与掩码回传保护。
- 时钟回拨会让 lastRun 落在未来,interval 任务在时钟追上前完全不执行而界面无异常。
- 玩家名正则 \w{1,16} 不含 . 和 -,Bedrock/Floodgate 玩家不入在线列表和 playtime,
但封禁接口用的是 [\w.-] —— 同一个名字能被封却不算在线。
- 中途放弃的 import 空壳删不掉(state=importing,唯一出路 finalize 要有效压缩包),
只能靠重启面板让 state 变回 stopped 这种非显然的办法。DELETE 放行 importing,
并补上 archiveBusy 守卫(压缩进行中删实例会让 tar 写出残缺产物)。
- installInstance 的 motd 是死参数,调用方从来不传。
## 测试
smoke 73 → 85 项。新增 securitySuite(CSRF 四条 / SSRF 三条 / 凭据掩码三条 /
放弃的空壳可删两条),并在 collabRoleSuite 里补 operator 启停与 viewer 读凭据。
注:collabRoleSuite 需要一个已存在的实例,空面板上会跳过 —— 这批权限用例
已另行对运行中的面板手工验证(viewer /logs 200、/rcon /properties /files/content
403;operator /server/stop 200、PATCH 403、clone 403、建备份 200)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
对上一轮审计逐条自行验证后修复。有一条审计结论被我推翻、一条被改小,都在下面标出。
🔴 面板会被打进重启循环(已实测复现)
src/instance.js的_tickTps用了props['rcon.port'],而该方法里根本没有props变量(同名局部变量在另一个方法里):三个条件叠起来才致命:
enable-rcon=true+ 有密码。面板有「一键开启 RCON 并生成随机密码」,README 力推的 TPS 监控也必须开 RCON。uncaughtException兜底 → 抛出即进程退出。autorestart拉起后resumeInstances()又把服务端拉起来 → 10 秒后再崩,max_restarts: 10之后彻底停摆。修了根因,另加两道纵深防御:指标循环 per-instance try/catch、
server.js全局兜底。🔴 协作者收不到任何实时推送
resolveAllowed原样展开collaborators,而bus.js用includes(username)做字符串比对。但PUT /collaborators一律写成{name, role}对象:控制台不滚动、状态不刷新,而 REST 正常 —— 表现为「页面像卡住了」,极易误判成前端问题。
🟠 权限分档
OPERATOR_WRITES启停正则不匹配/start,真实路由是/:iid/server/:action。方向是失效关闭(不越权),但 operator 档形同虚设 —— 想给启停权只能给 manager,而 manager 能改文件改配置,净效果是权限被迫放大/rcon(明文密码)、/properties、/files/content、/files/download、/backups/:id/download全在 viewer。「只读」档能把实例全部秘密拿走clone缺isOwnerOrAdmin🟠 CSRF / SSRF
CSRF:全站无防护,只有
SameSite=Lax。新增 Origin/Sec-Fetch-Site校验。放行 Bearer Token(浏览器不会自动附带,跨源加头会触发预检)与无头的非浏览器客户端 —— smoke 全程用裸 fetch,73 条老用例一条没红,证明脚本没被误伤。SSRF:新增
utils.checkOutboundUrl,拒绝环回/私有/链路本地(含169.254.169.254云元数据)/CGNAT/组播,IPv4-mapped IPv6 按内嵌 v4 判定,并对 webhook 关掉重定向跟随。ARCHITECTURE 里那句「别把面板变成内网探测器」之前只做到了挡协议。🟠 配额
inspectBackup改用tar -tzvf拿真实未压缩体积,按净增长校验(恢复是覆盖式的,同等大小的包净增长≈0,不该被拦)。file.size,并在下载中按已写字节持续复查(声明体积可能是假的)。其余
notify 凭据明文回显 · 时钟回拨让 interval 任务永久静默停摆 · 玩家名正则
\w不含.(Bedrock 玩家能被封却不算在线)· 放弃的 import 空壳删不掉 · 删实例缺archiveBusy守卫 ·motd死参数state不落盘 ——meta()不含它,构造函数硬编码stopped。重启面板就能删。 准确说法是"不重启就删不掉",是 UX 毛病不是死锁。仍然修了,没理由让人为删一个空壳去重启面板。motd,只是个永远为 undefined 的死参数,无用户可见影响。验证
npm test:85 passed, 0 failed(原 73)。新增securitySuite:CSRF 四条 / SSRF 三条 / 凭据掩码三条 / 放弃的空壳可删两条collabRoleSuite在空面板上会跳过(它需要已存在的实例),所以那批权限用例这次没真正执行。已对运行中的面板手工验证:viewer/logs200、/rcon/properties/files/content403;operator/server/stop200(修复前是 403)、PATCH 403、clone 403、建备份 200tar -tzvf解析含空格文件名实测正确,体积精确🤖 Generated with Claude Code