跳到主要内容

原型污染

· 阅读需 4 分钟
const isObject = (obj) => obj !== null && typeof obj === 'object';

function merge(a, b) {
for (const attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
}
merge({}, JSON.parse('{"__proto__": {"admin": 1}}'));
console.log(({}).admin); // 1 触发原型污染

// 解决方案1 使用[jsonschema](https://imweb.io/topic/56b1b4bb5c49f9d377ed8ee9)对传入的JSON做格式校验
// 解决方案2 Object.freeze(Object.prototype);
// 解决方案3 在上面的merge函数中对key做判断
// 解决方案4 使用无对象原型Object.create(null)
// 解决方案5 使用Map来存取对象

众所周知js通过原型链串起了js的父类和子类,实例和构造函数,所有的对象可以说都是挂载一颗原型树下的。那么理论上如果一个原型发生了变化(比如增加或者删除了一属性),所有依赖于该原型的子对象都会受到影响,原型污染说的就是这件事情。

上述的例子中就展示了js的原型污染。

merge会递归的把b中的属性存入a中,当一个空对象与一个key为proto的对象合并时,虽然使用for in对空对象遍历时不会出现proto属性,但是上面是对另一个对象进行for in 遍历,是能够遍历出proto的key的,而{}.proto虽然不会被遍历到,但是是可以取到的,并且也是一个对象,那么就会进行更深层的合并,之后会把admin属性挂载到Object.prototype上面,造成原型污染,导致之后新建的对象可以通过原型链获取到admin属性

为了解决原型污染的问题

第一,可以通过限制用户的json输入,来获得我们想要的json格式的json数据,比如'{"proto": {"admin": 1}}' 就不是我们想要的json数据,那么可以使用jsonschema库来限制json输入

第二,因为上面我们进行merge的目标对象是个空对象,所以才会出现在访问proto时通过原型链造成了污染,那么可以将空对象替换为Object.create(null),使a.proto判断为undefined而不是一个对象

第三,因为上述merge函数对对象属性的无差别遍历导致了原型污染,所以可以在深层合并时对attr做进一步的判断,如果key为proto,就不进行深层遍历

第四,我们对Object.prototype的修改造成了Object.prototype的改变(说的有点奇怪),是因为Object.prototype是可以被修改的,所以可以通过Object.freeze(Object.prototype)来防止Object.prototype被改变

第五,虽然和现在讨论的对象污染没太大关系,可以使用Map数据结构来替代对象进行存取,好处有两点:一,key不仅限于string和symbol类型,二,存取更快,底层对其进行了优化