0x00 前言
与之前所学过的基于类的面向对象语言不同的,JavaScript是一门基于原型的的语言——每个对象拥有一个原型对象。这表示JS中并不像之前那些面向对象语言一样,先创建类再创建一个对象(虽然之后引入了”class“,是只是语法糖,实际还是基于原型),在JS中,有“一切皆是对象”的说法。那什么是“原型“,JS中又是如何不同寻常地实现面向对象的呢?
0x01 原型
初见原型
在JS中,一切皆是对象,所以函数也是对象,它也可以有自己的属性。而每个构造函数都有一个特殊的属性:prototype
,即原型
,而“原型”也是一个对象,所以原型也可以有自己的属性。我们可以使用如下的代码在浏览器控制台中打印出一个函数的原型:
function foobar(){
this.bar="bar";
this.hello = function(){
console.log("HELLO");
}
};
console.log(foobar.prototype);
在原型对象中,有两个属性。construtor
指向了构造函数本身,__proto__
将在下文解释。
实例化一个对象
上文我们查看了构造函数的原型对象,但我们还不知道它的具体作用,先回到本文的问题,“JS是怎么实现面向对象的呢”,也即“JS是如何实现继承等面向对象特性的呢?”。
使用下面代码实例化一个对象(JS中的对象是通过构造函数实现),并在控制台中输出。
function foobar(){
this.bar="bar";
this.hello = function(){
console.log("HELLO");
}
};
var myfoo = new foobar();
console.log(myfoo);
可以看到,myfoo实例化对象中的__proto__
居然和构造函数的原型对象一样,这有什么用呢?这是不是与属性和方法的继承有关呢?我们可以做个实验:先实例化一个对象,再给构造函数的原型添加一个属性,然后再调用这个属性看看
实验一下
实验代码如下:
function foobar(){
this.bar="bar";
this.hello = function(){
console.log("HELLO");
}
};
var myfoo = new foobar();
foobar.prototype.test = "THIS IS TEST";
console.log(myfoo.test);
我们可以惊奇的发现,我们先实例化后,再给构造函数的原型添加一个属性,居然可以在实例化对象中调用。
并且,当我们再次查看__proto__
时,发现也它发生了变化,自动加上了我们为构造函数原型对象添加的属性。(也就是说始终和构造函数的原型对象同步变化)
所以,实验结果表明,构造函数的原型对象确实是与实例化对象的属性和方法的继承有关。
;并且实例对象的__proto__
是指向构造函数的原型对象prototype
的;另外,我们隐隐感觉到,实例对象调用原型中新加的属性(或许是所有属性/方法)似乎应该也是通过了__proto__
(毕竟新属性在这里很显眼的出现了,并没有出现在和”bar”同层次)。
0x02 原型链
上文提到实例对象调用属性/方法,似乎与__proto__
有着一丝关系,实际上,这两者确实是有关系,并且关系还不小——这便是原型链。
知识点来咯
我们以上文的例子进行分析,来理清原型的作用以及原型链的意义。当我们调用实例化对象myfoo
中的属性(/方法,下文省略)时,JS会先在myfoo
自己这先寻找这个属性,当不存在时,便会在__proto__
中(即foobar
的原型对象)寻找,若再没找到,则会在__proto__
的__proto__
中(即foobar
的原型对象的__proto__
)寻找。默认情况下,函数原型对象的__proto__
指向的是object.prototype
。此时如果还是没找到,因为object
的__proto__
不存在,就会停止寻找,得出结论,这个属性是undefined
。
以上一层层的__proto__
便构成了原型链
。
大白话的总结
可以看到,原型prototype
似乎就类似于装备的蓝图,而__proto__
就类似于一个点了就会跳转到对应蓝图的链接(这样说可能不太准确,因为实际上是myfoo.__proto__=foobar.prototype
)。当我们想制作一件🐂🍺的装备时,居然发现材料缺少😡,然后我们就点到原材料上,居然发现原材料也要合成(🤦♂️),然后我们点进原材料的原材料,居然发现还是材料不足💢,于是要我们点进原材料的原材料的原材料的原材料的原材料(艹),最后显示首充即送100个。哈哈,看着火气就直接上来了🔥🔥🔥
0x03 原型链污染
上文我们在探究的实验过程中,好像发现一个事情,我们先实例化了一个对象,然后再给构造函数的原型对象加上一个属性,然后再用实例化对象去调用新属性,居然可以调用成功(好像Python也有动态添加属性,学艺不精,不确定)。我们是修改了构造函数达成了修改实例对象的效果,那我们是不是可以试想一下下面的这个实验,是否能通过实例对象来实现修改构造函数,从而影响之后创建的实例化对象。
新实验
- 先创建一个构造函数
Father
,然后实例化一个对象son1
。 - 为
son1
的__proto__
添加一个新属性 - 再利用
Father
实例化一个对象son2
- 通过
son2
调用新属性,观察是否调用成功
代码
function Father(){
}
let son1 = new Father();
console.log(son1.foo);
son1.__proto__. foo = 'foobar';
console.log(son1.foo);
let son2 = new Father();
console.log(son2.foo);
实验结果
可以看到,通过son1
确实可以成功修改来自同一“祖先”的son2
原型链污染
其实上面的实验便基本阐述了“原型链污染”的过程,通过修改一个对象“父类”的原型,来影响其他来自同一“祖先”的对象。
0x04 攻击实例
懒🐕 ,将在后面的文章,收集一些CTF题目进行实践。