夜雨聆风学习资料网

ARTICLE · 1338992

1101 Star,这个播放器把插件关进256MB沙箱:5条边界能直接抄

1101 Star,这个播放器把插件关进256MB沙箱:5条边界能直接抄

内部工具最怕的不是没人用,是有人提需求。加一个 if,再加一个 if,半年后主程序里躺着七个业务方的判断,谁都不敢删。

这个 1101 Star 的开源播放器走了反方向:主程序不写业务,插件只能拿到它明确申请过的东西。

我把它的插件层拉下来跑了一遍测试:45 个用例全过,1.23 秒。顺手写了几个探针,专挑越权的用法。


一、插件是一个文件夹,导出什么就做什么

plugin.json 加一个 index.js(或 .ts),不用构建,同一份代码在四个平台都能跑。

README 里列了 46 个可选导出:search、stream、lyrics、playlists……主程序按你导出了什么决定显示哪些页面。没导出 search,就没有搜索框,而不是弹一个"功能未实现"。

代码里的判断只有一句:!p.exports[method] 就返回 ErrUnsupported。我调用一个它没导出的 deletePlaylist,回来的是:

not supported by this source

仓库里的 demo 插件 9,155 字节,把能写的导出全写了一遍,permissions 是空的——不申请权限也能跑,只是没有网络。

主程序自己也不内置任何音源,要听歌先装插件。这个取舍很硬:内容合规和版权风险一起挪出了主程序。代价是每个源都要自己处理登录、分页、错误码。

这条最难抄也最值钱:能力不是配置表里写"支持/不支持",而是你交出去的东西里有没有那个函数。


二、权限只有两类,不写就什么都没有

permissions 里只有 network 和 process 两个字段。network 是域名白名单,*.example.com 表示子域,* 表示任意。一个都不写,fetch 什么都够不到。

我写了个只申请空 permissions 的插件,在 search 里 fetch 一个域名,回来的是一句 TypeError: fetch failed。不是"你没权限",就是普通的 fetch 失败。

这件事有两面:→ 越权确实被挡住了→ 但插件作者排错时会一脸懵,所以文档必须写清"没申请网络时 fetch 一定失败",否则你会收到一堆"这 API 挂了"的 issue

process 是能启动的程序,同样白名单。判定很细:带斜杠的按路径匹配,不带斜杠的按 PATH 里的名字匹配。bin/helper-* 只匹配插件目录下的,ffmpeg 只匹配 PATH 里那个。

名字一样但位置不对的程序,不算。而且要用户点过一次授权,才额外放开 127.0.0.1 和 localhost——因为插件要跟这个子进程说话。

还有一个容易漏的字段:requestHeaders。插件自己的请求能带 Cookie,但主程序替它下载封面和音频时,得单独给规则,只对指定域名、指定用途生效,值里可以引用用户填的设置或插件自己的存储。

权限不是"给不给网络"这么粗,而是"哪个域名、哪类资源、带哪个头"。


三、卡死和慢,是两件事

调用超时 30 秒。超时之后它不立刻杀,而是再给 1 秒看事件循环还能不能接新任务:

→ 接不了:判死循环或吃满内存,日志打 stuck; restarting it,运行时丢掉重建→ 接得了:说明只是等 IO 慢,继续跑

两个测试正好覆盖这两条:TestStuckPluginRestarts 花 1.05 秒,TestWaitingPluginIsLeftRunning 0.05 秒过。

我塞了一个 while(true){} 进去,31.003 秒返回 the plugin did not answer search in time,插件被重启。

我在这台机器上跑出来的探针输出:越权、未导出、坏 id、死循环

很多插件系统只有一个超时,把慢请求当崩溃。用户切下一首歌,登录态就掉了。


四、上限写成常量,不写在文档里

memoryLimit = 256MB,超过直接重启运行时。安装包 maxArchive 1GB、解压后 2GB、最多 5 万个文件。

插件 id 有正则约束,不能用 ../ 爬到自己数据目录外面。我试了 id=../evil,报的是 a plugin can not be called "../evil"。

minAppVersion 也拦在加载前:给 0.1.9 装一个要求 9.0.0 的插件,报 Y needs MyGo Music 9.0.0 or later,不是跑起来再崩。

读源码时记了三个小细节:插件数据目录是 0700;写 JSON 先落 .part 再 rename,崩了只丢最后一次;启动时某个插件加载失败只记一行日志,不会把别的插件一起拖下水。

它没有给插件的东西也很清楚:文件系统、剪贴板、任意 shell 都没有,host 只暴露存数据、读设置、两个哈希、开浏览器、起子进程这几样。清单短,才好审。

五、升级时多要权限,要重新问一次

asksForMore 只比 network 和 process 两个列表:新版本多要一个域名、多要一个程序,就停下来重新弹安装确认。老版本要过 * 的,以后不再问。数据目录不动,只换代码。


抄到自己项目里,我留了五条

→ 能力 = 导出:有没有这个函数,不用配置开关→ 权限白名单,默认拒绝:越权的错误要能被人看懂,看不懂就写进文档→ 超时与卡死分开判:先看事件循环活不活,再决定杀不杀→ 上限写成常量:内存、包大小、文件数、id 规则→ 升级比权限差集:多要一项就让用户重新点一次

把插件的"能做什么"写成白名单,比写一堆开关好维护

什么时候别学它

单机脚本、一个人维护的 CLI,加这层就是自找麻烦。

对性能敏感的东西也别照搬:这里每次调用都要 JSON 编码进去、再编码出来,一帧几十次的音频处理,开销会盖过收益。

还有一层成本在维护:插件层有 2,438 行实现、1,611 行测试,46 个用例。你抄的是边界,不是这四千行代码——边界只有五条,四千行可以按自己项目重写。

总结——插件系统真正难的不是"怎么让它跑起来",是"怎么让它跑坏了也不带走主程序"。你把边界画在函数、权限、超时、资源四处,剩下的交给插件作者。

你们内部工具的扩展点是怎么开的?评论区聊聊踩过的坑

📂 更多同类内容,点合集「程序员效率工具库」

如果觉得有用,点个「在看」↘

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料