2021-04-01收到了github的邮件,还以为是关于愚人节的(误),提醒我有一条Dependabot alerts,包依赖存在安全问题,当然这不是第一次了,正常情况我批准merge掉后就不会关心了,不过我发现这个包是y18n,这让我感到奇怪,这个包的功能非常简单,其实就是i18n,做语言国际化用的,逻辑应该非常简单,怎么会出安全问题呢?
我找到了这个安全问题的具体信息,他的CVE ID为CVE-2020-7774
,并且上报了SNYK,代码为SNYK-JS-Y18N-1021887
,SNYK对此安全问题给出了7.3的高分,并将安全类别划分为CWE-400
(资源消耗不受限制),如此严重的问题是怎么回事呢?
找到具体的修复PR,在https://github.com/yargs/y18n/pull/108,commit只有一个,为https://github.com/yargs/y18n/pull/108/commits/e90e8ce71b2fd5aa27c5109884ea47525fde961f,打开这个commit,发现其实主要只改了一行代码,加了两个检测原型的单元测试,这一行代码修改如下
- this.cache = {
}
+ this.cache = Object.create(null)
可能有人不明白这两行代码有什么区别,使用{}
创建的对象自带原型属性,而Object.create(null)
是干净的,在console中就可以试出来,输入{}
,点开返回的内容,发现其中只有一个__proto__
属性,点开这个属性可以发现其中还有constructor
,hasOwnProperty
等等,而输入Object.create(null)
,返回的是一个没有任何属性的空的{}
。
为什么带__proto__
的对象有安全隐患呢,原因很简单,__proto__
是所有对象共享的,而不是一个对象独占,举一个很简单的例子,在console运行如下code
let foo = {
}
let bar = {
}
foo.__proto__.isBad = true
bar.isBad
//true
我们发现bar.isBad
得到了true
,其实此时任何原型对象都有了一个isBad
,这就足以说明其危险性,而Object.create(null)
是没有__proto__
的,即使强行赋一个__proto__
的值,也不会改变其他对象的__proto__
当然在很多时候我们发现不少项目都有直接写{}
的习惯,他们都有安全问题吗?当然不是,如果你能完全控制好自己的对象,不使其收到用户输入与环境的影响,那自然是没问题的,但上文提到的cache
对象并不是这样,大致阅读下index.ts
的代码就可以发现,外部可以给予它任何属性,没有做任何的检查,这当然是存在严重的问题的,使用y18n的程序只需要被注入一小段以__proto__
为locale代码,系统很快就会奔溃掉。
关于使用Object.freeze防止原型链污染
其实除了用Object.create(null)
创建对象以外,如果适当的时机使用Object.freeze()
冻结__proto__
,这个问题也不会发生
const foo = {
}
const bar = {
}
foo.progress//progress of this foo object
foo.__proto__ = Object.freeze(foo.__proto__)
foo.__proto__.isBad = true
//foo.__proto__.isBad is undefined
freeze掉__proto__
之后再对__proto__
进行修改时,不会产生任何效果,因为foo已经被冻结,此时即使对象暴露在外部,全局__proto__
指针也是安全的,它事实上已经是只读的,但请注意只冻结对象是没有用的,Object.freeze无法冻结整个属性链,例如
const a = Object.freeze({
b: {
c: 1}})
a.b.c = 2
这样修改是成功的,因为Object.freeze只冻结了b属性,但b自己也是一个对象,它的属性c是不会被冻结的,在__proto__
上也是一样的,
const foo = Object.freeze({
})
const bar = {
}
foo.__proto__.isBad = true
bar.isBad
//true
__proto__
本身被冻结了,foo.__proto__ = xx
是不会生效的,但是对__proto__
中属性的操作仍然都是有效的
其他注意事项
- 不少情况下,Map都要比Object更好,应该经常注意是否具备用Map替代Object的情况,在2016年ES6兴起的时候,国外就有社区对这个问题进行了大量交流,也有很多大牛发文响应这一观点,近年来也有很多相关的中文文章
- 对外部JSON输入进行安全验证,例如使用jpv 一类的验证器,不过这些组件的安全问题也是值得关注的,例如
CVE-2020-17479
这样伪装成数组绕过验证的神奇漏洞 - 防止直接的代码注入,例如在web项目的console中警告用户不要输入未知代码,例如Facebook使用多国语言在console中警告用户停止输入未知内容的行为,在electron应用中应该在发布时关闭develop面板
FYI
本文写作于2021年4月2日并发布于lyrieek的掘金,于2023年7月16日进行修订发布于lyrieek的阿里云开发者社区。