JavaScript原型
在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个[[Prototype]]
内部属性,这个属性所对应的就是该对象的原型。[[Prototype]]
作为对象的内部属性,是不能被直接访问的,所以为了方便的查看一个对象的原型,Firefox和Chrome提供了__proto__
这个非标准的访问器。每个对象拥有一个原型对象,对象以其原型为模版,从原型继承方法和属性,原型对象也可能拥有原型,并从中继承方法和属性,层层递推,这种关系被称为原型链。
其中,foo
是一个Foo
类的实例,有两个属性:bar、[[Prototype]],其中bar是我们构造函数中定义的,而[[Prototype]]就是Foo.prototype
,在JavaScript中,每个函数都有一个prototype属性,当一个函数被用作构造函数来创建实例时,该函数的prototype属性将被作为原型赋值给所有对象实例,也就是说,所有实例的原型引用的是函数的prototype属性。遵循ECMAScript标准,Foo.[[Prototype]]
符号是用于指向 Foo
的原型。从 ECMAScript 6 开始,[[Prototype]]
可以通过 Object.getPrototypeOf()
和 Object.setPrototypeOf()
访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__
。
如上图所示是一个原型链图,其中Parent是构造函数,p1是通过Parent实例化出来的一个对象。当谈到继承时,JavaScript只有一种数据结构:对象。每个实例对象都有一个私有属性(__proto__
)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__
),层层向上知道一个对象的原型对象为null
,根据定义null
没有原型,并作为这个原型链中的最后一个环节。
1 | var Parent = function(){ |
prototype属性
prototype
是函数独有的属性,从 上图可以看到它从一个函数指向另一个对象,代表这个对象是这个函数的原型对象,这个对象也是当前函数所创建的实例的原型对象。有了prototype
我们不需要为每一个实例创建重复的属性方法,而是将属性方法创建在构造函数的原型对象上(prototype)。那些不需要共享的才创建在构造函数中。
proto属性
__proto__
属性时对象(包括函数)独有的,从图中可以看到__proto__
属性是从一个对象指向另一个对象,即从一个对象指向该对象的原型对象,Parent.prototype
上添加的属性和方法叫做原型属性和原型方法,该构造函数的实例都可以访问调用。那这个构造函数的原型对象上的属性和方法,怎么能和构造函数的实例联系在一起呢,就是通过__proto__
属性。每个对象都有__proto__
属性,该属性指向的就是该对象的原型对象。__proto__
通常称为隐式原型,prototype
通常称为显式原型,那我们可以说一个对象的隐式原型指向了该对象的构造函数的显式原型。那么我们在显式原型上定义的属性方法,通过隐式原型传递给了构造函数的实例。这样一来实例就能很容易的访问到构造函数原型上的方法和属性了。
constructor
constructor
是对象才有的属性,它是从一个对象指向一个函数的,指向的函数就是该对象的构造函数,每个对象都有构造函数。
原型链污染
对于JavaScript而言,很少有真正的私有属性,类的所有属性都允许被公开的访问和修改,包括proto、构造函数和原型,攻击者可以通过注入其他值来覆盖或污染这些proto、构造函数和原型属性,然后,所有继承了被污染原型的对象都会受到影响,原型链污染通常会导致拒绝服务、篡改程序执行流程、RCE等。
如下定义一个递归合并函数merge()
1 | const merge = (target, source) => { |
对于上述代码,如果job对象是由用户输入的,并且输入是任意的,那么我们可以输入一个含有proto属性的对象,那当合并的时候就可以把Person的原型给修改了。
1 | let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}'); |
修改后的结果如下图所示
这里需要注意的是,只有不安全的递归合并函数才会导致原型链污染,非递归的算法并不会导致原型链污染,例如JavaScript自带的Object.assign
1 | function Person(name,age,gender){ |
如何利用
- 字符串可以被解析为方法或对象,例如
JSON.parse
进行解析,shvl
库使用点对属性操作。 - 对象的键和值都可控,
target[key] = value
如何防御
- 禁止操作
constructor
- 禁止操作
prototype
- 禁止操作
__proto__
JS原型小结
__proto__
、constructor
属性是对象所独有的prototype
属性是函数独有的- 在js中函数也是对象的一种,那么函数同样也有属性
__proto__
、constructor
- 所有的引用类型都有一个
__proto__
属性(也叫隐式属性,是一个普通的对象) - 所有的函数都有一个
prototype
属性(也叫显式属性,是一个普通的对象) - 当试图得到一个对象属性时,如果这个对象本身不存在这个属性,会从它的构造函数的
prototype
属性中去寻找
参考资料
- https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
- https://www.freebuf.com/articles/web/275619.html
- https://www.cnblogs.com/wilber2013/p/4924309.html
- https://segmentfault.com/a/1190000021232132
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain