原型链污染
参考文章
JavaScript原型链污染原理及相关CVE漏洞剖析 - FreeBuf网络安全行业门户
Node.js原型链污染的利用 - FreeBuf网络安全行业门户
用前端原型链漏洞污染拿下了服务器 - 腾讯云开发者社区-腾讯云 (tencent.com)
原型
- 三个名字
- 隐式原型:所有引用类型(函数、数组、对象)都有
__proto__
属性,例如arr.__proto__
- 显式原型:所有函数拥有
prototype
属性,例如:func.prototype
- 原型对象:拥有
prototype
属性的对象,在定义函数时被创建
- 隐式原型:所有引用类型(函数、数组、对象)都有
JavaScript
中每个对象都拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链(prototype chain)
示例:
- 数组原型链:arr → Array.prototype → Object.prototype
1 | var arr = ['a', 'b', 'c'] |
- 对象原型链:object → class.prototype → Object.prototype
1 | class Person { |
person
是一个Person
类的示例,有四个属性:name
、age
、gender
、proto
。前三个是在构建函数中定义的,第4个属性proto
就是Person.prototype
这个机制的作用是什么呢?当我们访问person
的一个属性时,浏览器首先查找person
是否有这个属性.如果没有,然后浏览器就会在person
的proto
中查找这个属性(也就是Person.prototype
)。如果Person.prototype
有这个属性,那么这个属性就会被使用。否则,如果Person.prototype
没有这个属性,浏览器就会去查找Person.prototype
的proto
,看它是否有这个属性,依次类推。默认情况下,所有类的原型属性的proto
都是Object.prototype
。
JavaScript的对象在访问自身没有的变量或者方法时,会向其父类(也就是原型)寻找,没找到再向其父类寻找,对于本例来说就是:
person.introduce();
person 自身没有introduce()方法,向Person类寻找,Person类找到则返回,没找到继续向Object类寻找。
- 攻击的例子
1 | var a = { 1 : "a", 2 : "b"}; |
原型式继承
由于JavaScript没有类似Java中的Class概念,继承都是由原型链来实现的。实现继承的方法很多,例:
1 | function Person(name, age, gender) { |
创建一个构造函数Teacher
,其中调用Person.call
运行Person
构造函数。Person.call
函数的第一个参数指明了在运行这个函数时想对“this”指定的值,此处是Teacher
的this
,即Person
构造函数里对this.name
赋值,实际上是对Teacher
的this
赋值。接着用Object.create()
方法创建一个新对象proto
,这个对象使用Person.prototype
来作为它的proto
。然后将Teacher
和proto
相互“关联”,proto
的构造函数是Teacher
,Teacher
的原型是proto
。这样就实现了Teacher
对Person
的继承,此时Teacher.prototype
如下图所示:
Teacher.prototype的proto属性是Person.prototype,原型链也就多了“一层”。
Teacher → Teacher.prototype → Person.prototype → Object.prototype
原型链污染
在 JavaScript发展历史上,很少有真正的私有属性,类的所有属性都允许被公开的访问和修改,包括 proto,构造函数和原型。攻击者可以通过注入其他值来覆盖或污染这些 proto,构造函数和原型属性。然后,所有继承了被污染原型的对象都会受到影响。原型链污染通常会导致拒绝服务、篡改程序执行流程、导致远程执行代码等漏洞。
原型链污染的发生主要有两种场景:不安全的对象递归合并和按路径定义属性。
其他语言,例如 Java,可以通过 public
、(default)
、promoted
、private
的访问限制符避免此类问题。对应着 Java来看,JavaScript里面的所有属性都是 public属性
1 | class Student { |
不安全的对象递归合并
1 | const merge = (target, source) => { |
(说明:JavaScript自带的Object.assign只能深拷贝第一层,第一层以后还是浅拷贝。因此很多开发者都会自定义一些递归合并函数。)
如果有这样一个场景:job对象是由用户输入的,并且用户可以输入任意对象。那么我们输入一个含有“proto”属性的对象,那合并的时候就可以把person的原型给修改了。 此时如果我们新建一个Person对象,可以发现,新建的对象的原型已被修改。
需要注意的是,只有不安全的递归合并函数才会导致原型链污染,非递归的算法是不会导致原型链污染的,例如JavaScript自带的Object.assign。
示例:
1 | function Person(name,age,gender) { |
此时再新建一个Person对象,可以发现,原型未被污染。
这是因为Object.assign在合并时,对于简单类型的属性值得到的是深拷贝,如string,number。如果属性值是对象或其他引用类型,则是浅拷贝。上面例子中的proto就是一个浅拷贝,合并后person的原型只是指向了一个新对象,即{“x”: 1},Person.prototype没有受到影响。