JS59 对象的扩展、密封和冻结

JavaScript中,我们可以修改和重写一切未经保护的对象,同样,他人也可以随意重写我们所定义的对象。一般来讲,我们不应该重写他人的对象,这会导致代码很难维护。

不要重写他人的对象

不要重写他人的对象,因为别人可能使用了你修改的对象,修改后的对象的行为有可能对他人的功能产生巨大的影响。例如:

1
2
3
4
5
6
7
8
window.myAlert = alert;

alert = function (msg) {
if (typeof msg === 'string' || typeof msg === 'number') {
return myAlert(msg)
}
return console.log(msg)
}

上面的代码重写了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'use strict';
let p = { a: 1 };

console.log(Object.isExtensible(p));
// true

Object.preventExtensions(p);

console.log(Object.isExtensible(p));
// false

p.a = 123;
console.log(p.a);
// 123

delete p.a;
console.log(p.a);
// undefined

p.__proto__.x = 66;
console.log(p.x);
// 66

p.__proto__ = {};
// Uncaught TypeError: #<Object> is not extensible

p.b = 999
// Uncaught TypeError: Cannot add property b, object is not extensible

Object.seal()

Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置(阻止删除属性)。当前属性的值只要可写就可以改变。尝试添加或删除现有属性,将会在严格模式下抛出错误。

Object.seal()改变了属性的访问器属性configurable,让该属性变成不可配置。

可以通过Object.isSealed()判断属性是否被封闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
'use strict';
let p = { a: 1 };

console.log(Object.isSealed(p));
// false

console.log(Object.getOwnPropertyDescriptors(p).a.configurable);
// true

Object.seal(p);

console.log(Object.isSealed(p));
// true

console.log(Object.getOwnPropertyDescriptors(p).a.configurable);
// false

Object.defineProperty(p, 'a', {
enumerable: false
})
// Uncaught TypeError: Cannot redefine property: a

p.a = 123;
console.log(p.a);
// 123

p.__proto__.x = 66;
console.log(p.x);
// 66

delete p.a;
console.log(p.a);
// Uncaught TypeError: Cannot delete property 'a' of #<Object>

p.__proto__ = {};
// Uncaught TypeError: #<Object> is not extensible

p.b = 999
// Uncaught TypeError: Cannot add property b, object is not extensible

Object.freeze()

Object.freeze()是最强大的,它可以冻结一个对象,冻结后的对象不能添加新的属性、不能删除已有属性,也不可配置行(不能修改该对象已有属性的可枚举性、可配置性、可写性)、不能修改已有属性的值。

可以用Object.isFrozen()判断对象是否被冻结。

可以利用这个方法将对象彻底冻结,使其符合const变量的含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
'use strict';
let p = { a: 1 };

console.log(Object.isFrozen(p));
// false

console.log(Object.getOwnPropertyDescriptors(p).a.configurable);
// true

Object.freeze(p);

console.log(Object.isFrozen(p));
// true

console.log(Object.getOwnPropertyDescriptors(p).a.configurable);
// false

Object.defineProperty(p, 'a', {
enumerable: false
});
// Uncaught TypeError: Cannot redefine property: a

p.a = 123;
console.log(p.a);
// Uncaught TypeError: Cannot assign to read only property 'a' of object '#<Object>'


p.__proto__.x = 66;
console.log(p.x);
// 66

delete p.a;
console.log(p.a);
// Uncaught TypeError: Cannot delete property 'a' of #<Object>

p.__proto__ = {};
// Uncaught TypeError: #<Object> is not extensible

p.b = 999
// Uncaught TypeError: Cannot add property b, object is not extensible

Lodash的安全漏洞

Lodash中的defaultsDeep方法就因为会被利用来意外修改Object.prototype对象,从而产生安全隐患。

正常使用的时候,这个方法会将第二个参数的可枚举属性分配到第一个参数所有解析为undefined的属性上。

1
2
_.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } });
// { 'a': { 'b': 2, 'c': 3 } }

当攻击者构造如下的字符串:

1
2
3
const payload = '{"constructor": {"prototype": {"toString": true}}}'

_.defaultsDeep({}, JSON.parse(payload))

执行的结果是将{}.constructor也就是Objectprototype.toString属性改写为true,这样当其他对象调用toString方法时,就执行了攻击者的代码。

Loadash的修复方案是在合入属性的过程中遇到constructor__proto__等敏感属性,就会退出。

作为业务开发者,预防方法有:

(1)可以使用前面提到的Object.freeze来冻结原型,使原型无法被修改

(2)解析用户输入的时候,建立JSON schema,过滤敏感键名

(3)规避不安全的递归合并,对敏感键名做跳过处理

(4)在特殊情况下,使用Object.create(null)创建无原型对象,或者使用Map对象

JavaScript的JSON.parse方法则不存在这个漏洞,它默认忽略了__proto__属性

参考