在2017年,一项名为"Prototype Pollution"的安全漏洞被公开披露,该漏洞利用了JavaScript中的原型继承机制,通过修改原型链上的属性来影响目标对象的行为。这种攻击技术使得攻击者可以篡改或扩展目标对象的原型链,从而导致意外行为或安全漏洞的产生。
原型链污染可能导致严重的安全漏洞和意外行为。攻击者可以通过修改原型对象来添加、修改或删除目标对象上的属性和方法,甚至可能篡改内置对象的原型。这可能导致应用程序中的任意代码执行、信息泄露、拒绝服务等问题。
JavaScript中的原型(prototype)是一个对象,在对象创建时会自动关联到该对象上。每个对象都有一个原型对象,它充当了对象之间的连接,形成了原型链。
原型链是一种机制,它允许对象通过继承属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript引擎会沿着原型链向上查找,直到找到该属性或方法为止。
例如定义一个Student类,查看它的原型
var student=new Student('张三',18,'男')
console.log(student)
输出结果如下:
可以看到,student是Student类的一个实例,我们可以看到定义的三个属性:name,age,sex, 在这三个属性之外我们可以看到还有一个prototype属性,它指向该对象的原型对象Student.prototype。Student.prototype的constructor属性是Student构造函数,__ proto __ 属性是Object.prototype,具体内容从以下输出也可以看出来。
通过上面的分析我们可以画出它的具体原型链结构图
当我们访问student的一个属性时,浏览器首先查找student是否有这个属性。如果没有,浏览器就会在student的__ proto __ 中查找这个属性(也就是Student.prototype)。如果Student.prototype有这个属性,那么这个属性就会被使用。如果Student.prototype也没有,浏览器就会去查找Student.prototype的__ proto __,看它是否有这个属性,依次类推。在默认情况下,所有类的原型属性的 __ proto __ 都是Object.prototype。
接下来创建一个构造函数Person,其中调用Student.call运行Student构造函数。
Person(name,age,sex) {
Student.call(this,name);
Student.call(this,age);
Student.call(this,sex)
}
然后奖Person的原型对象设为Student,就实现了Person对Student的继承
Person.prototype=new Student()
console.log(Person.prototype)
输出的结果如下:
我们可以看到Person的原型对象(Person.prototype)变成了Student.prototype
相当于在原有原型链的基础上再加一层
原型链污染主要是攻击者通过修改原型链上的对象来改变应用程序的行为,或者利用原型链上的对象来执行恶意代码。
原型链污染可通过一下几种方式实现:
1、修改Object.prototype或其他原型对象:攻击者可以直接修改Object.prototype或其他原型对象,添加或修改属性和方法。这样,所有继承自该原型的对象都会受到影响。
2、修改Object.prototype.constructor:攻击者可以修改Object.prototype.constructor,将其指向恶意代码或其他构造函数。这样,通过原型链创建的对象在调用构造函数时可能会执行攻击者指定的恶意代码。
3、使用__proto__属性:攻击者可以利用__proto__属性(非标准的属性)来修改对象的原型链。通过修改__proto__属性,攻击者可以将对象的原型链指向任意对象,从而影响对象的属性和方法的继承。
merge函数是一种常见的用于合并对象的函数。它将源对象的属性合并到目标对象中,可以用于深度合并两个对象的属性。
以下是一个示例的merge函数实现:
function merge(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (typeof source[key] === 'object' && typeof target[key] === 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
这个merge函数接受两个参数:目标对象(target) 和源对象(source)。它遍历源对象的属性,并将每个属性合并到目标对象中。
如果属性的值是对象,并且目标对象中相应的属性也是对象,则递归调用merge函数来深度合并这两个对象。
如果属性的值不是对象,直接将源对象的属性值赋给目标对象的相应属性。
最后,返回合并后的目标对象。
通过调用merge函数,可以将一个或多个源对象的属性合并到目标对象中,实现对象属性的合并和覆盖
let a = {}
let b = {"id": 1, "__proto__": {"no": 2}}
merge(a, b)
上面的式子中我们实现了两个对象实例 a 和 b ,并将 a 和 b 的属性进行了合并,再对象b中我们定义了一个 proto 属性,想让 b 在合并的过程中直接实现对a的原型对象 a.__proto __ 的更改,当我们输出 a 的时候发现b的属性都合并过来了,但是 {no : 2} 却成了a的一个属性,而不是 a 的原型对象
当我们再输出 a.__ proto __ 果然,a的原型对象是 Object.prototype,不是 {no : 2}
这是为什么呢?
因为上述代码中 b 对象是直接使用对象字面量的方式创建的,__proto__会被视为内置属性解析掉,然后把 {b:2} 传给 a 对象,因此应该用 JSON.parse 将其解析为对象
var safeObj = require("safe-obj");
var obj = {};
console.log("Before : " + {}.polluted);
safeObj. expand (obj,'__proto__.polluted','Yes! Its Polluted');
console.log("After : " + {}.polluted);
查看index.js 中expand函数的定义
该函数有三个参数: obj,path,thing
当我们调用 expand 函数,传参如下:
obj={},path="__proto__.polluted",thing="Yes! Its Polluted"
当执行完 path.split(‘.’) 时 path 被分为两部分,其props数组的值如下:
props=(2){"__proto__","polluted"}
执行完shift语句之后
prop="__proto__",props="polluted"
下面再次调用expand函数的时候就相当于调用
expand(obj[__proto__],"polluted","Yes! Its Polluted")
然后再次递归,此时
props=["polluted"]
因为props里面只有一个元素,所以props.length===1成立,执行obj[props.shift()]=thing,相当于执行 obj[proto][“polluted”]=“Yes! Its Polluted”,造成原型污染。
为了防止原型链污染,开发者应该遵循以下最佳实践:
避免直接修改Object.prototype或其他原型对象,以及它们的属性和方法。
使用Object.create(null)创建纯净的对象,不继承任何原型对象。
谨慎处理不可信数据,避免将其用作原型链的一部分。
使用Object.freeze()或其他方法冻结对象,防止其被修改。
定期更新和审查第三方库,确保其不会引入原型链污染的安全问题。
通过遵循安全原则和最佳实践,可以减少原型链污染的风险,并保护应用程序的安全性和稳定性。