原型与原型链
原型
每一个javascript对象(除null)创建的时候,就会与之关联一个对象,这个对象就是原型,每一个对象都会从原型中"继承"属性
prototype
与__proto__
javascript中,每一个函数都有一个prototype
属性,指向该函数的原型对象
javascript中,每一个对象(除null)都会有一个__proto__
属性,指向该对象的原型
function Person(){
}
var person=new person();
console.log(person.__proto__==Person.prototype); //true
用一张图来表示就是
值得注意的是,Person也有__proto__属性
因为JavaScript中函数也是对象,任何函数都是Function类型的实例(包括Function本身)
constructor
每个原型都有一个constructor属性,指向该关联的构造函数
function Person() {
console.log("person");
}
var person= new Person(); //输出test
//以上代码创建了一个Person函数,同时Person也是一个类,可以像别的语言中那样为这个类实例化一个对象person
/*在实例化person的时候,Person()也同样执行了.
这可以联想到构造函数,构造函数在的特性就是在new一个对象的时候执行.
所以Person()函数与person这个类的关系就很明显了,前者是后者的构造函数
通过constructor这个属性可以查看对象的构造函数*/
console.log(person.constructor); //function Person()
实例与原型
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
function Person() {
}
Person.prototype.name = 'Xux';
var person = new Person();
person.name = '000';
console.log(person.name) // 000
delete person.name;
console.log(person.name) // Xux
我们给实例对象person
添加了name
属性,当我们打印person.name
的时候结果为Xux。
但是当我们删除了person
的name
属性时,读取person.name
,从person
对象中找不到name
属性就会从person
的原型也就是person.__proto__
,也就是Person.prototype
中查找,结果为000。
当获取person.constructor
时,因为person没有constructor属性,会从person.__proto__
也即Person.prototype
中寻找
因此person.constructor===Person.prototype.constructor
原型的原型
原型也是一个对象,是通过对象Object函数生成的
var obj=new Object();
obj.name="Xux";
concosle.log(obj.name); //Xux
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。
Object.prototype.__proto__没有原型 即值为null
上面所述可用如下这张图来表示
图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
原型链污染
原型链污染是什么
一个简单的例子:
//foo是一个JavaScript对象
let foo={bar:1};
console.log(foo.bar); //值为1
foo.__proto__.bar=2;
console.log(foo.bar); //值仍然为1
let zoo={};
console.log(zoo.bar); //值为2
虽然zoo是一个空对象,但是zoo中bar的值为2
因为前面我们修改了foo的原型foo.__proto__.bar=2
而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染
哪些情况下原型链会被污染
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)
虽然o1和o2合并成功了,但是原型链没有被污染
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}})
中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],__proto__
并不是一个key,自然也不会修改Object的原型。
可以使用JSON解析,在JSON解析的情况下,__proto__
会被认为是一个真正的"键名",而不是原型,所以在遍历o2的时候会存在这个键
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)
此时成功将原型链污染
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
Object clone
function clone(obj){
return merge({},obj);
}
Attack
###拒绝服务
JS中的Object默认带了一些属性,如toString和valueOf,利用原型链污染他们,可能导致整个程序停止运行
{}__proto__.toString="123";
{}__proto__.valueOff="123";
###属性注入
如果污染了如cookie等,可能会造成所有用户公用一个session
{"__proto__":{"cookie":"sess=114514"}
防御
1.原型冻结
使用Object.froze()
冻结对象
冻结后的对象无法再更改,无法添加,编辑或删除其中的属性
2.使用Map代替Object
需要使用Key/Value模式时,尽量使用Map
//js创建map对象
var map = new Map();
//将键值对放入map对象
map.set("key",value)
map.set("key1",value1)
map.set("key2",value2)
//根据key获取map值
map.get(key)
//删除map指定对象
delete map[key]
或
map.delete(key)
//循环遍历map
map.forEach(function(key){
console.log("key",key) //输出的是map中的value值
})
3.Object.create(null)
可以用JavaScript创建没有任何原型的对象 : Object.create(null)
,用Object.creat
创建的对象没有__proto__
和constructor
以这种方式创建对象可以帮助减轻原型污染攻击
参考:
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
https://www.cnblogs.com/l0nmar/p/13951739.html