JavaScript原型链污染攻击

JavaScript原型

在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个[[Prototype]]内部属性,这个属性所对应的就是该对象的原型。[[Prototype]]作为对象的内部属性,是不能被直接访问的,所以为了方便的查看一个对象的原型,Firefox和Chrome提供了__proto__这个非标准的访问器。每个对象拥有一个原型对象,对象以其原型为模版,从原型继承方法和属性,原型对象也可能拥有原型,并从中继承方法和属性,层层递推,这种关系被称为原型链。

1

其中,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__

2

如上图所示是一个原型链图,其中Parent是构造函数,p1是通过Parent实例化出来的一个对象。当谈到继承时,JavaScript只有一种数据结构:对象。每个实例对象都有一个私有属性(__proto__)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上知道一个对象的原型对象为null,根据定义null没有原型,并作为这个原型链中的最后一个环节。

1
2
3
4
var Parent = function(){

}
var p1 = new Parent();

prototype属性

prototype是函数独有的属性,从 上图可以看到它从一个函数指向另一个对象,代表这个对象是这个函数的原型对象,这个对象也是当前函数所创建的实例的原型对象。有了prototype我们不需要为每一个实例创建重复的属性方法,而是将属性方法创建在构造函数的原型对象上(prototype)。那些不需要共享的才创建在构造函数中。

proto属性

__proto__属性时对象(包括函数)独有的,从图中可以看到__proto__属性是从一个对象指向另一个对象,即从一个对象指向该对象的原型对象,Parent.prototype上添加的属性和方法叫做原型属性和原型方法,该构造函数的实例都可以访问调用。那这个构造函数的原型对象上的属性和方法,怎么能和构造函数的实例联系在一起呢,就是通过__proto__属性。每个对象都有__proto__属性,该属性指向的就是该对象的原型对象。__proto__通常称为隐式原型,prototype通常称为显式原型,那我们可以说一个对象的隐式原型指向了该对象的构造函数的显式原型。那么我们在显式原型上定义的属性方法,通过隐式原型传递给了构造函数的实例。这样一来实例就能很容易的访问到构造函数原型上的方法和属性了。

constructor

constructor是对象才有的属性,它是从一个对象指向一个函数的,指向的函数就是该对象的构造函数,每个对象都有构造函数。

原型链污染

对于JavaScript而言,很少有真正的私有属性,类的所有属性都允许被公开的访问和修改,包括proto、构造函数和原型,攻击者可以通过注入其他值来覆盖或污染这些proto、构造函数和原型属性,然后,所有继承了被污染原型的对象都会受到影响,原型链污染通常会导致拒绝服务、篡改程序执行流程、RCE等。

如下定义一个递归合并函数merge()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const merge = (target, source) => {
for (const key of Object.keys(source)) {
if (source[key] instanceof Object) Object.assign(source[key], merge(target[key], source[key]))
}
Object.assign(target || {}, source)
return target
}
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
}
let newperson=new Person("test1",22,"male");
let job=JSON.parse('{"title":"Security Engineer","country":"China","male":"true"}');
merge(newperson,job);
console.log(newperson);

对于上述代码,如果job对象是由用户输入的,并且输入是任意的,那么我们可以输入一个含有proto属性的对象,那当合并的时候就可以把Person的原型给修改了。

1
let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}');

修改后的结果如下图所示

3

这里需要注意的是,只有不安全的递归合并函数才会导致原型链污染,非递归的算法并不会导致原型链污染,例如JavaScript自带的Object.assign

1
2
3
4
5
6
7
8
9
function Person(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
}
let person1=new Person("test1",22,"male");
let job=JSON.parse('{"title":"Security Engineer","country":"China","__proto__":{"x":1}}');
Object.assign(person1,job);
console.log(Person.prototype);

4

如何利用

  • 字符串可以被解析为方法或对象,例如JSON.parse进行解析,shvl库使用点对属性操作。
  • 对象的键和值都可控,target[key] = value

如何防御

  • 禁止操作constructor
  • 禁止操作prototype
  • 禁止操作__proto__

JS原型小结

  • __proto__constructor属性是对象所独有的
  • prototype属性是函数独有的
  • 在js中函数也是对象的一种,那么函数同样也有属性__proto__constructor
  • 所有的引用类型都有一个__proto__属性(也叫隐式属性,是一个普通的对象)
  • 所有的函数都有一个prototype属性(也叫显式属性,是一个普通的对象)
  • 当试图得到一个对象属性时,如果这个对象本身不存在这个属性,会从它的构造函数的prototype属性中去寻找

参考资料