「这是我参与2022首次更文挑战的第34天,活动详情查看:2022首次更文挑战」。
一、背景
是个周五,周五就应该有周五的态度!摸鱼专家表示今天安全团队的态度不太端正。。。忽然有人听见喊我老板和我的名字,让我俩看下。。。
我已经嗅到了危险的气息,我预感到又要周五上线。果不其然,我就听到了如下对话:
“这个包我们的项目中也没有用到啊,package.json 没有搜到”!
“是啊,似乎是个三方库的依赖,可能是依赖的依赖”,
“这没法升级啊,这咋办啊”,
“要不x老板和x老师你们看下?”
而我的反应:What? When ? Where ? How ....
这个时候我的老板转过头来说:今天凌晨两点,安全团队扫描了我们的 git
仓库,发现使用项目使用的 underscore
是带有安全漏的版本,并且发送了邮件,这个邮件居然限期当日修复,否则报请我上级批准....
修复建议就是升级到没有问题的版本,npm install xx@xx.xx.xx
????
我一想,这个事情肯定不是执行个把的 npm install
就能解决的问题,我长相不帅气,声音不磁性,有家有室的,蹦迪谁会叫我呢?
虽然这个时候心里想摸鱼,但是领导待咱不薄,咱摸鱼的时候领导从不说啥。
其实听到上面的对话,我心里就已经盘算好了解决方案,所以领导说完情况,我说这个问题好办,不出意外的话中午饭之前应该能搞定,但是可能涉及基础工具升级,影响范围待评估,上线时间得拉 QA
评估一下
二、安全漏洞
安全漏洞的英文翻译为:Security Vulnerability
本次遇到的安全问题是 underscore 这个库的安全问题,国家信息漏洞官方描述:存在代码注入漏洞,攻击者可利用该漏洞容易通过模板函数执行任意代码。
这里还有个工具查询该安全漏洞的具体情况的工具,传送门synk.io:
在 synk.io 还给出了具体使用该漏洞注入的例子:
在上面的例子中,可以看到调用 child_process
(Node.js 原生模块),调用系统命令创建文件,这是一个相当危险的后门。
但是从 synk.io
中可以看出,这个问题从 2012
年就有,直到 2021.3
才修复。所以你说紧急?紧急! 当然庆幸我们的业务没有收到这个漏洞的影响,真不知道该感谢老天爷,还是感谢老天爷....
三、排查问题
当我查到上面的问题细节的时候我就更自信了,尤其是看到 underscore
官方已经修复了。这是为啥呢?
因为问题不是我们最先发现的,漏洞早在一年前就修复了。所以依赖 underscore
的三方库理想情况下已经完成了升级,从前面背景描述可以看出问题的症结所在:
- 项目未直接依赖
underscore
,但是确实安装了,表现就是package.json
dependencies/devDependencies
没有,但是package-lock.json
中有; - 有修复漏洞的版本,但是不知道如何入手升级;
解决第一个问题并不难,npm
提供了这种能力,查询一个包在项目中的依赖关系:npm ls <pkg-name>
;
$ npm ls underscore
这个命令返回一个依赖树,你会发现这两个包依赖了一个共同依赖:normalize-package-data
,而 noraml-package-data
依赖了 underscore
;
注意此时,我们的项目并不直接依赖 normalize-package-data
,他们也是依赖的依赖,项目中直接依赖的是他们的上层:eslint-plugin-import、node-sass
;
四、解决方案
面对上面的问题,入手点很明确:从项目中找到依赖 underscore
的三方包,然后去查看这些三方包的 change-log
或者 commit
记录,看看是不是把 underscore
升级到修复漏洞后的版本。
所以问题就变的简单起来了,尝试升级 eslint-plugin-import、node-sass
,所以问题又变成了个把的 npm install
命令;
从 package-lock.json
中搜索,在我们的项目使用的版本: eslint-plugin-import@2.23.4
,node-sass@5.0.0
;接着去 github
搜索这两个包,选择距离漏洞版本最近的 release
:
eslint-plugin-import@2.24.0
和 node-sass@6.0.0
,当然要看下这个 release
的时间晚于漏洞发现时间。然后查看二者 package.json
文件,发现这俩包在新版本中移除了对 normalize-package-data
这个依赖,这个是真的狠。。。
然后,然后?本地测试一下没问题,提测,汇报,齐活儿~
五、临时解决方案
进度条告诉你不简单,今天这个漏洞的发展完全符合我的预期,即 underscore
漏洞发现时间较早,依赖 underscore
的 eslint-plugin-import
和 node-sass
早就在我司发现漏洞之前发现了漏洞,所以就大大方方的 npm install
就可以完成工作了;
但是,假如一个开源库 a
,有个开源库 b
依赖 a
。漏洞你首先发现的,当然你肯定不会首先告诉美国,你刚刚提了个 issue
,a
修复了,然后你也给 b
提了 issue
,但是 b
的维护者因为打架被抓了。反正 b
的作者就是很倒霉,就是没办法升级 a
的版本。
还有另外一个场景,你使用的开源框架开发你的业务,你恰好命中了它的 bug
,而 bug
来自另一三方包,框架也在迭代暂时不方便发版,而你又不得不解决的时候,你就需要下面的临时方案了;
5.1 package.json 的 overrides
先上文档传送门npm package.json overrides
原文:If you need to make specific changes to dependencies of your dependencies, for example replacing the version of a dependency with a known security issue, replacing an existing dependency with a fork, or making sure that the same version of a package is used everywhere, then you may add an override. Overrides provide a way to replace a package in your dependency tree with another version, or another package entirely. These changes can be scoped as specific or as vague as desired.
三流翻译凑合看吧:
当你需要指定一个依赖的依赖版本时,比如你需要解决一个安全问题,替换一个依赖为他的一个 fork 版本,并且这个包被安装的到处都是(即很多包都依赖了他),这时候你需要一个 overrides
overrides 提供了一种将你的依赖树中的包替换成另一个版本,或者将这个包完全替换成另一个的能力。这种更改可以是明确范围的,当然不想明确范围也是可以的;
npm
的 package.json
中设置 overrides
字段强制某个依赖包以指定的版本被安装,会忽略原有的版本控制;
以下的例子和解释来自文档,并加入一些个人理解:
5.1.1 无条件覆盖
以下写法标识将会确保你的依赖中所有的 foo
包的安装版本为 1.0.0
,无论你之前的依赖是什么版本,这就是一种不明确范围的写法(....or as vague as desired
);
{
"overrides": {
"foo": "1.0.0" // 所有的依赖 foo 的包都强制 1.0.0,这种就是不明确作用范围的写法
}
}
这是一种简写形式,他的完整版如下:
使用完整版可以控制 foo
本身为 1.0.0
,还可以设置 foo
的后代依赖的 bar
的版本为 1.0.0
。
后代:原文的解释...also making 'bar' at any depth beyond 'foo' also 1.0.0
,翻译,任何深度大于 foo
的 bar
的版本为 1.0.0
,原文说的是深度,因为依赖树是一个有深度的结构,所以原文用了 depth
;而我们用的后代,也是想表达相同的意思;
{
"overrides": {
"foo": {
".": "1.0.0", // . 代表 foo 本身
"bar": "1.0.0" // bar 表示的是 foo 的后代中的 bar
}
}
}
5.1.2 后代包的替换
当 foo
是 bar
的后代包的时候才覆盖为 1.0.0
,这个相当于重申一遍上面的全写形式,不过这里说的就是明确作用范围(be scoped as specfic...
)
{
"overrides": {
"bar": {
"foo": "1.0.0" // 只覆盖 bar 后代中的 foo 包为 1.0.0
}
}
}
这个嵌套可以是任意多层(原文...Keys can be nested to any arbitrary length
),下面的示例表示覆盖 foo
的版本为 1.0.0
,仅当 foo
被 bar
依赖且 bar
又被 baz
依赖的情况下。
{
"overrides": {
"baz": {
"bar": {
"foo": "1.0.0"
}
}
}
}
5.1.3 覆盖指定版本号的子依赖的版本
作为 overrides key
的被覆盖版本号的包名,可以携带一个版本号或者版本号范围;下面的例子,覆盖 foo
版本为 1.0.0
,当它是版本号为 2.0.0
的 bar
的后代的情况下;
{
"overrides": {
"bar@2.0.0": {
"foo": "1.0.0"
}
}
}
原文中说了一种比较极端的场景:你的项目依赖了 foo
包,还依赖了一个 baz
,而 baz
也依赖了 foo
,你需要让 baz
下面的 foo
版本和顶层的 foo
包版本一样。
这种场景比较常见与公司内网包被重复依赖的问题,记得之前说过一个相同包有的装在项目根node_modules,有的装在某个包私有的 node_modules 的问题,当时我同事就想让私有的那一份和外面的一样,这个当时我以为做不到,看起来打脸了,无知是原罪啊。。。overrides
给出了解决方案:
让 overrides
设置某个 包的的 value
时,使用 $
前缀引用一个外层依赖的包名,表示用外面包的版本覆盖当前包的版本,下面例子;
{
"dependencies": {
"foo": "^1.0.0"
},
"overrides": {
// $foo 标识上面 dependencies 中的 foo 的版本号 1.0.0,你可以认为这是个变量;
"foo": "$foo",
"bar": "$foo"
}
}
5.2 npm-force-resolutions
前面的 package.json
的 overrides
实现了问题,但是这个篇幅也很明显,那么有简单做法吗?
原文:This packages modifies package-lock.json to force the installation of specific version of a transitive dependency (dependency of dependency), 译: 这个包通过修改 package-lock.json 的方式让嵌套依赖安装指定版本...
5.2.1 用法:
- 在
package.json
中加入resolutions
属性,它的值是个对象;对象中的key
是想要覆盖掉的包,值是版本号;
"resolutions": {
"hoek": "4.2.1"
}
- 在
package.json
的scripts
加入preinstall
,preinstall
是npm 的生命周期脚本
,preinstall
表示对应的脚本将在npm install
执行前运行;
"scripts": {
"preinstall": "npx npm-force-resolutions"
}
- 接着就是调用
npm install
就可以了;
六、总结
本文我们从 underscore
的漏洞开始,讲述了如何解决安全漏洞:
通过 npm ls <pkg-name>
查询依赖关系,如果这些包已经升级到了没有安全漏洞的版本,我们就直接升级,这是最繁琐也是优解决方案;
接着我们有讲了两种临时解决方案:package.json
的 overrides
字段、npm-force-resolutions
包。无论是哪一种临时方案,都不应该首先使用,因为这会带来潜在的风险,越基础的包,越不要这么做。