coa 和依赖锁定

题图:iliched @ unsplash.com

1、周下载量 700w+ 的 npm 包 coa 昨晚(2021.11.4)让大片前端社区挂了,比如 Bigfish 2 受影响的依赖链是 老框架 > @svgr/webpack > @svgr/plugin-svgo > svgo@1.3.2 > coa。应该是被黑客偷了发布权限,针对不同的 major 和 mirror 分别发布带问题的版本,其新增版本在 preinstall hook 里执行 start /B node compile.js & node compile.js

2、目的是挂马,但所幸遇到的是笨黑客,挂马没成功。安装依赖时直接报了 Error: Cannot find module 'path/to/compile.js' 的错误,惊动了整个前端社区,没成功的原因是发布时忘记在 files 里加上 compile.batcompat.js,看到下图,我直接笑出了声。

3、大家可能都有这样的经历。1)本来项目跑地好好的,重装依赖后就挂了 2)跑了多年的老项目,新建的迭代突然就不能跑了,甚至工具 owner 早就离职了 3)睡一晚起来,发现 ESLint 规则变了,CI 跑不过了。这里有工具本身升级的问题,同时更大可能的则是三方依赖或其依赖更新引起的问题,还记得之前的 leftpad 事件吗?还记得 babel 两天更 8 个 bugfix 版本吗?

4、为什么有这个问题?抛开攻击导致的安全问题,还有 semver 理想和现实的差距。理想的 semver,是完全遵循 major break-change,mirror feature,bugfix bugfix 的世界,而现实的 semver,则是 major break-change,mirror break-change,bugfix break-change。造成的影响可大可小,构建报错、CI 报错、白屏、线上故障都可能发生。

5、是个问题就有解,社区已经有了不少方案。比如 cnpm 的 bug-versions 可以忽略问题库,各个包管理都有提供 lock 功能来锁定整体依赖,package.json 中的 resolutions 配置可以锁定局部依赖,patch-package 库可以做代码级的临时修复。

6、此外还有个解法是框架层直接锁依赖。业务代码的依赖链通常是 业务代码 > 框架 > 框架依赖 > 间接依赖,在框架层锁定所有依赖是比较合适的,并且这个锁不止锁直接依赖,也要锁间接依赖。而 npm 并没有提供锁的方案,所以目前社区流行的方案是把依赖打包来达到锁间接依赖的目的。

7、打包分两种。小型工具可以把自己合依赖一起打包,比如 rollup、rome、vite 等;大型框架通常需要拆包,并且有 node 侧和 browser 侧的依赖需要分开处理,通常会采用打包依赖的方式,比如把 node_modules/foo 编译到 compiled/foo,比如 next.js、umi。

8、打包后你会发现依赖其实没想象中那么大。比如 webpack 打包后 3M、babel 全家桶打包后 3M、lodash 打包后 93K、axios 打包后 31K。

9、打包依赖最大的好处是让框架和业务项目的安全稳定,比如 coa 的这次问题,对于 umi 3 以及内部依赖 umi 3 的业务框架和业务代码都没有任何影响。此外还有安装时 0 peerDependencies 警告、安装依赖时文件尺寸和数量下降,安装速度等提升。

10、同事的总结是:0 error  0 warning 0 hijacking,更安全和稳定,依赖全锁定,再也不用担心 Babel 深夜发版,工具十年后依旧可用。