原型链污染简介(Prototype chain pollution)

什么是prototype和__proto__?

在JavaScript中,我们如果要定义一个类,就需要以定以”构造函数”的方式来定义

function Foo(){
this.bar = 1
}

new Foo()

Foo函数的内容,就是Foo类的构造函数,而this.bar就是Foo类的一个属性

一个类必然有一些方法,类似属性this.bar,我们也可以把方法定义在构造函数内部

function Foo(){
this.bar = function(){
console.log(this.bar)
}
}

(new Foo()).show()

但是这样就会在每次新建一个Foo对象的时候,this.show = function…就会被执行一次,这样子这个show方法就是和对象进行绑定,而不是和”类”进行绑定了

怎么避免重复,就需要使用原型(prototype)了

function Foo(){
this.bar = 1
}
Foo.prototype.show = function show(){
console.log(this.bar)
}
let foo = new Foo()
foo.show()

可以认为原型prototype是类Foo的一个属性,而所有Foo由类 实例化出来的对象,都将拥有这个属性中的所有内容,包括其中的变量和方法,以上代码中的foo,创建的时候就拥有了foo.show()方法

可以通过Foo.prototype来访问Foo类的原型,但是Foo实例化出来的对象,不能通过prototype来访问原型。这种情况下就要使用到__proto__了。

一个Foo类进行实例化出来的foo对象,可以通过foo.__proto__属性来访问Foo的原型

所以可以得到

foo.__proto__ == Foo.prototype

image-20210919195220277

prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法

一个对象的__proto__属性,指向这个对象所在的prototype属性

简单理解就是prototype是用来创造对象用的,往前走

__proto__是用来回朔用的,往回找

function Father(){
this.first_name = 'Donald'
this.last_name = 'Trump'
}

function Son(){
this.first_name = 'hhh'
}

Son.prototype = new Father()

let son = new Son()
console.log(`name:${son.first_name} ${son.last_name}`)

Son类继承了Father类的last_name属性

对于对象son,在调用last_name的时候,JavaScript引擎会进行的操作如下:

  • 在对象son中寻找last_name
  • 如果找不到,就到son.__proto__中寻找last_name
  • 还找不到,就到son.__proto__.__proto__中寻找last_name
  • 就这样一直往上找,一直找到null宣告结束

image-20210919201638852

image-20210922184614279

P神说,大家要记住以下三点

记住记住记住

  • 每个构造函数{constructor}都有一个原型对象{prototype}
  • 对象的__proto__属性,指向类的原型对象prototype
  • JavaScript使用prototype来实现继承的机制

什么是原型链污染

现在我们知道了foo.__proto__指向的是Foo类的prototype。那么如果我们修改了foo.__proto__中的值,是不是就可以修改Foo类?

let foo = {bar:1}//先创建一个对象

console.log(foo.bar)//js先在foo对象中找bar,找到了,值为1

foo.__proto__.bar = 2//修改foo的原型,其实是修改了Object这个类,给这个类增加了bar=2

console.log(foo.bar)//js还是在foo对象中找bar,并不会往后的prototype找bar

let zoo = {}//创建一个空的zoo对象

console.log(zoo.bar)//因为zoo是一个空对象,所以第一次寻找找不到,只能往上找,找到了Object的bar
1
1
2

这样子,在一个应用中,如果攻击者控制并且修改了一个对象的原型,那么就可以影响所有和这个对象来自同一个类、父祖类的对象,这种攻击方式就叫做原型链污染。

哪些情况下原型链会被污染?

现在我们知道了控制对象的 __proto__ ,即可影响该实例的父类,那么要如何控制 __proto__ 呢?

JS中针对对象的复制分为浅拷贝深拷贝,简单来说:

浅拷贝 只是将指向对象的指针复制了过去,不论如何拷贝,这些拷贝都指向同一个引用,一旦被修改,所有引用都会变化;

深拷贝 则是要将目标对象完完全全的“克隆”一份,占据自己的内存空间。

实现深拷贝,一种常见的方式是:递归遍历需要复制对象的所有属性,并且全部赋值给新的空对象,实际上创建了一个新的对象。而浅拷贝就是引用。

在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?

这里做一个举例

  • 对象merge
  • 对象clone

以对象merge为例子,我们想象一个简单的merge函数

function merge(target,source){
for(let key in source){
if(key in source && key in target){
merge(target[key],source[key])
}else{
target[key] = source[key]
}
}
}

在合并的过程中,存在赋值的操作target[key] = source[key],那么,这个key如果是__proto__,是不是就可以原型链污染呢?

我们用如下代码实验一下:

let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

//1 2
//undefined

虽然合并在了一起,但是并没一被污染。因为我们用JavaScript创建o2的过程(let o2 = {a: 1, “__proto__“: {b: 2}})当中,__proto__被认为是o2本对象的原型,此时又会遍历o2的所有键名,拿到的是a和b两个键,__proto__并不是一个key,自然也不会修改Object的原型(我们自己创建的对象都是以Object为原型创建来的)

let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)

o3 = {}
console.log(o3.b)

//1 2
//2

此时利用JSON.parse方法,这个方法可以将JSON字符串解析为值或对象。所以在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

再来个例子

上面那个是通过__proto__来实现漏洞,还有另一种方式:重载构造函数

当我们将constructor和prototype嵌套作为键名的时候

function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
let o1 = JSON.parse('{"constructor": {"prototype": {"hello": 1}}}')
merge({},o1)

let o2 = {}
console.log(o2.hello)

实例 constructor 的 prototype ,和实例的__proto__指向一致。由于 merge 操作的解析是递归的,这种方式同样也会污染 Object