原型与原型链

原型

每一个javascript对象(除null)创建的时候,就会与之关联一个对象,这个对象就是原型,每一个对象都会从原型中"继承"属性

prototype__proto__

javascript中,每一个函数都有一个prototype属性,指向该函数的原型对象

javascript中,每一个对象(除null)都会有一个__proto__属性,指向该对象的原型

function Person(){
}
var person=new person();
console.log(person.__proto__==Person.prototype); //true

用一张图来表示就是
1.png

值得注意的是,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。
但是当我们删除了personname属性时,读取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

上面所述可用如下这张图来表示

2.png

图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

原型链污染

原型链污染是什么

一个简单的例子:

//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合并成功了,但是原型链没有被污染

3.jpg

这是因为,我们用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)

4.jpg

此时成功将原型链污染

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