原型链污染学习
学习参考:p佬的深入理解 JavaScript Prototype 污染攻击
工具:WebStorm
prototype和__proto__
js中,如果要定义一个类,需要以定义构造函数
的方式来定义
先整个Tool类
function Tool(){
this.id = 1
}
new Tool()
Tool函数的内容,就是Tool类的构造函数,其中this.id是Tool类的一个属性
然后我们在这个类里定义一个方法,但是每当我们调用show方法的时候就会执行一次this.show = function()
,这时候,这个方法是绑定在对象上面
的,而不是绑定在类上面
的
function Tool(){
this.id = 1
this.show = function(){
console.log(this.id)
}
}
(new Tool()).show()
接下来,如果我们希望创建类的时候只创建一次show方法
,这时需要用到原型prototype
function Tool(){
this.id = 123
}
Tool.prototype.show = function show(){
console.log(this.id)
}
var t = new Tool()
t.show()
运行结果
这个时候我们发现,我们没有在Tool的构造函数中定义show方法,而是用类似于属性的方式在类外定义Tool.prototype.show
,然后我们新建一个Tool类对象,我们仍然可以使用show方法
这是为什么呢?
根据P佬的解释:我们可以认为原型prototype
是类Tool
的一个属性,而所有用Tool
类实例化的对象,都将拥有整个属性中的所有内容,包括变量和方法
(这里有点像子类继承父类的关系,一个新建的子类对象可以使用在父类中定义的方法)
但是注意这里,我们是使用Tool.prototype
来访问Tool
类的原型,但是该类对象是不能通过prototype来访问原型的,这个时候就用到了__proto__
if (t.__proto__ == Tool.prototype){
console.log("二者相等")
}
运行结果
所以是两种访问原型的方法(以Tool为例):
- 通过
Tool.prototype
来访问Tool的原型 - 通过类对象
t.__proto__
来访问Tool的原型
一个对象的__proto__属性,指向这个对象所在的类的prototype属性
实现js的继承机制
使用prototype
function father(){
this.id = "father_id"
this.name = "father_name"
}
function son(){
this.id = "son_id"
}
son.prototype = new father()
let s = new son()
console.log(s.id)
console.log(s.name)
运行结果
这里我们看到,son类中是没有name这个属性的,然而我们却可以正常输出,而且输出的是father类的name属性值
这是因为son类继承了father类的name属性
使用__proto__
然后用__proto__
来实现上面的功能
function father(){
this.id = "father_id"
this.name = "father_name"
}
function son(){
this.id = "son_id"
}
// son.prototype = new father()
let fa = new father()
let s = new son()
s.__proto__ = fa
console.log(s.id)
console.log(s.name)
运行结果
对于son类的对象s,调用name的时候,实际上javascript引擎会进行如下操作:
- 在对象s中寻找name属性
- 找不到就去
son.__proto__
中寻找name属性 - 如果找不到,就去
son.__proto__.__proto__
寻找name属性 - 一直找下去,直到null结束。
Object.prototype.__proto__
就是null
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
注意,尽管s.__proto__ == son.prototype
,但是在具体使用的时候,如下(类
对类
,对象
对对象
)
//prototype使用方法
son.prototype = new father()
//__proto__ 使用方法
let fa = new father()
let s = new son()
s.__proto__ = fa
原型链污染
测试代码
let a = {id:1} //a是一个简单的js对象,是一个Object类的实例
console.log(a.id)
a.__proto__.id = 2
console.log(a.id)
let b = {} //b是一个空对象
console.log(b.id)
运行结果
第二次输出a.id
,结果是1,因为查找顺序,还是会最先
从a这个类对象中寻找id的值
而输出b.id,结果是2.是因为a是一个Object类的实例,a.__proto__.id = 2
实际上是对Object类增加了一个属性id
,且值为2
所以我们后来使用Object类创建b的时候,b对象虽然是空的,但是当输出b.id的时候,会向b.__proto__
中查找,也就是向Object类
中查找,得到id=2
原型链污染测试
p佬的文章,接下来分析控制数组的键名
测试
测试代码
var a1 ={a:1,b:2}
for(let key in a1){//key是键名
console.log(key)
console.log(b1[key])
}
运行结果
但是如果我们将a1变为{a: 1, "__proto__": {b: 2}}
var a1 = {a: 1, "__proto__": {b: 2}}
for(let key in a1){//key是键
console.log(key)
console.log(a1[key])
}
运行的结果为 a 1 b 2
我们此时遍历b1的所有键名,得到a,b ,而不是__proto__
p佬给出了解决方法
var a1 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
for(let key in a1){//key是键
console.log(key)
console.log(a1[key])
}
执行结果
一个merge函数,这段代码的意思是,将source数组的键值复制到target中
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 source = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
let target = {}
function merge(target, source) {
for (let key in source) {
console.log(key)
if (key in source && key in target) {//key既在source中,也在target中
merge(target[key], source[key]) //进里层迭代
} else {
console.log(key)
console.log(target[key])
console.log(source[key])
target[key] = source[key]//如果key不在target中,就将source的值传给target
}
}
}
merge(target,source)
console.log(target)
console.log(target.a, target.b)
console.log(source)
o3 = {}
console.log(o3.b)
运行结果
而这会导致Object类增加一个属性b,且值为2
所以最后我们使用Object类实例化o3,输出o3.b会输出2
真正执行的代码
要注意的地方是target[key] = source[key]
,真正的利用原理应该是这样的
let target = {}
target['__proto__']['b'] = 2
let a = {}
console.log(a.b)
运行结果