JS59 对象的扩展、密封和冻结
JavaScript中,我们可以修改和重写一切未经保护的对象,同样,他人也可以随意重写我们所定义的对象。一般来讲,我们不应该重写他人的对象,这会导致代码很难维护。
不要重写他人的对象
不要重写他人的对象,因为别人可能使用了你修改的对象,修改后的对象的行为有可能对他人的功能产生巨大的影响。例如:
1 | window.myAlert = alert; |
上面的代码重写了window.alert
方法,根据参数的类型做出了不同的反应,但是团队里其他开发者使用了alert
,这种行为很可能是他预料之外的。
而且为原生对象添加属性的行为也是有一定危险的,因为有可能和未来JavaScript的标准中的命名发生冲突。
阻止他人重写对象
同样的,有些情况下下,我们也不希望其他人随意重写我们定义的对象,一般会用到三个方法Object.preventExtensions()
、Object.seal()
和Object.freeze()
,这三个方法的异同:
方法 | 禁止添加属性 | 禁止删除属性 | 禁止修改属性 |
---|---|---|---|
Object.preventExtensions() |
是 | 否 | 否 |
Object.seal() |
是 | 是 | 否 |
Object.freeze() |
是 | 是 | 是 |
可以看出来,Object.freeze()
是最严格的。
Object.preventExtensions()
Object.preventExtensions()
将对象标记为不可扩展的,不能再添加新的属性,但是属性的删除和修改不受影响。当为不可扩展的对象添加属性时会静默失败,在严格模式下抛出错误。
Object.preventExtensions()
仅阻止为对象自身添加属性,但是属性仍然可以添加到原型对象。
可以通过Object.isExtensible()
判断对象是否可以扩展,一旦使其不可扩展,就无法逆转。
1 | ; |
Object.seal()
Object.seal()
方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置(阻止删除属性)。当前属性的值只要可写就可以改变。尝试添加或删除现有属性,将会在严格模式下抛出错误。
Object.seal()
改变了属性的访问器属性configurable
,让该属性变成不可配置。
可以通过Object.isSealed()
判断属性是否被封闭。
1 | ; |
Object.freeze()
Object.freeze()
是最强大的,它可以冻结一个对象,冻结后的对象不能添加新的属性、不能删除已有属性,也不可配置行(不能修改该对象已有属性的可枚举性、可配置性、可写性)、不能修改已有属性的值。
可以用Object.isFrozen()
判断对象是否被冻结。
可以利用这个方法将对象彻底冻结,使其符合const
变量的含义。
1 | ; |
Lodash的安全漏洞
Lodash中的defaultsDeep
方法就因为会被利用来意外修改Object.prototype
对象,从而产生安全隐患。
正常使用的时候,这个方法会将第二个参数的可枚举属性分配到第一个参数所有解析为undefined
的属性上。
1 | _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } }); |
当攻击者构造如下的字符串:
1 | const payload = '{"constructor": {"prototype": {"toString": true}}}' |
执行的结果是将{}.constructor
也就是Object
的prototype.toString
属性改写为true
,这样当其他对象调用toString
方法时,就执行了攻击者的代码。
Loadash的修复方案是在合入属性的过程中遇到constructor
和__proto__
等敏感属性,就会退出。
作为业务开发者,预防方法有:
(1)可以使用前面提到的Object.freeze
来冻结原型,使原型无法被修改
(2)解析用户输入的时候,建立JSON schema,过滤敏感键名
(3)规避不安全的递归合并,对敏感键名做跳过处理
(4)在特殊情况下,使用Object.create(null)
创建无原型对象,或者使用Map
对象
JavaScript的JSON.parse
方法则不存在这个漏洞,它默认忽略了__proto__
属性