原型链污染-概念解析
JavaScript语言的继承不通过class,而是通过原型对象(prototype)实现。
一、构造函数
JavaScript通过构造函数来生成新对象,构造函数相当于模板的作用。实例对象的属性和方法定义在构造函数中。
1 | function student(name, ID) |
这样通过构造函数给实例对象定义属性和方法是比较方便的。
但是这样会使得同一个构造函数的多个实例对象无法共享属性,那么在每一次生成新的实例对象时,这些对象之间本可以共享的属性都会重新进行一次内存分配并存储,造成资源的浪费。
1 | function student(name, ID) |
可以看到这里输出的false,因为进行比较的时候是比较这两个对象的内存地址,内存不一样说明两次分配内存是独立的,造成了资源的浪费,因为每一个实例对象的操作都是一样的。
二、prototype-原型对象
JavaScript通常被描述为一种基于原型的语言,即每一个对象都有一个原型对象。JavaScript的继承机制是原型对象的所有属性与方法,都被实例对象共享。原型是一块共享内存区域,存放各实例的共享元素。
JavaScript规定,每一个函数对象都有一个prototype属性,指向一个对象。对于普通函数几乎没用,对于构造函数,在生成新的实例对象时,该属性会自动成为实例对象的原型。
这里注意几点:
- 原型对象的属性不是实例对象的属性,修改原型对象的属性,所有的实例对象也会变动。
- 如果实例对象有某个属性或者方法,则不会再去原型对象寻找。
1 | function student(name, ID) |
三、原型链
1.原型链基本概念
每个对象都有自己的原型对象,原型对象也是一个对象,也有自己的原型对象,这会形成一个原型链。
- 当试图访问对象的某个属性时,会先在对象内部查找,没有就往原型查找,没有就接着往上,直到找到或者找到原型链的末尾,还是找不到返回undefined。
- 一层一层的往上查找,找到最后会找到Object.prototype,也就是Object的prototype属性,这也是为什么所有的对象都有valueOf方法和toString方法的原因。
- Object.prototype的原型对象是null,null没有属性和方法,没有自己的原型对象,也是原型链的末尾。
1 | function student(name, ID) |
2.constructor属性
prototype对象都有一个constructor属性,指向所在的构造函数。
- 这个属性可以知道某个实例对象是由哪个构造函数生成的,从而能够通过实例对象新建另一个实例。
- constructor属性表示的是原型对象和构造函数的关系,如果修改原型函数的指向时,需要一起修改constructor属性,防止引用出错。所以一般不直接修改原型函数指向,采用添加方法的形式操作。
1 | function student(name, ID) |
3.ptorotype和__proto__
对于构造函数而言,访问原型对象是通过prototype属性,但是对于构造函数创建的实例对象而言不是这么做的,而是通过__proto__属性。
| 特性 | prototype (显式原型) | proto (隐式原型) |
|---|---|---|
| 谁拥有它? | 函数 (构造函数/类) | 对象 (实例) |
| 它的作用 | 存放公共的方法和属性(相当于“公共库”)。 | 一个指针,指向构造函数的 prototype,帮对象找到它的“根”。 |
| 形象比喻 | 工厂的模具/设计图纸 | 产品身上贴的标签:“产自该模具” |
1 | function student(name, ID) |
其实cyt.__proto__.__proto__.__proto__的值为null,因为历史原因(类型标签的定义),类型为Object,此时访问cyt.__proto__.__proto__.__proto__.__proto__也就是null.__proto__就会报错。
四、原型链污染
原型链污染故名思及就是通过实例进行对其构造函数的原型对象的属性进行修改,从而污染后续构造函数新建的每一个实例。前提是这个实例没有自定义这个属性,即可实现覆盖。
1 | let father = {name: "father", age: 45}; |
可以看到已经实现了污染,新建的对象son的name值为son,而father对象因为自定义了这个属性所以没有被污染。
哪些情况会出现原型链污染呢?要找能控制对象的键名的操作,像merge()、clone()等危险函数
要注意:JavaScript 引擎对于对象字面量 和JSON.parse处理 __proto__ 键的方式完全不同
1 | function merge(target, source) { |
这里可以看到,o1的b属性被修改,a属性也被修改,理论上来说我们应该实现了原型链污染,但是从输出信息来看,只有o2的原型对象被修改,o1和o3的原型链并未被污染,这又是何意味?
当使用对象字面量 let o2 = {a: 1, “__proto__“: {b: 2}} 时,JavaScript 引擎进行了特殊处理:
o2的自有属性是a: 1。o2的内部原型([[Prototype]])被设置为{b: 2}。
for…in 的行为:
- 循环首先找到
o2的自有属性"a",将其复制到o1。 - 循环接着向上查找
o2的原型链。它在o2的原型对象{b: 2}上找到了属性"b"。 merge函数执行target[key] = source[key],即o1["b"] = o2["b"]。o2["b"]通过原型查找得到了值2。- 这个值
2被赋值给了o1,作为o1的自有属性。
原型链污染的发生,必须满足一个核心条件:攻击者成功地修改了所有对象共享的Object.prototype。
o2是用对象字面量创建的,"__proto__"没有成为o2的自有属性,因此for...in循环根本没有遍历到"__proto__"这个键。merge函数没有机会执行类似o1["__proto__"] = ...的赋值操作。- 因此,全局的
Object.prototype保持干净,没有被添加b属性。
那应该怎么操作呢?重点就是需要让__proto__被认为是一个键名,需要用到JSON解析
1 | function merge(target, source) { |
此时原型链真正被污染了,为什么用JSON解析就可以呢?
JSON 是一种数据交换格式,它不包含 JavaScript 的复杂逻辑(如原型链、函数等)。在 JSON 标准中,"__proto__" 没有任何特殊含义,它就是一个普通的字符串,就像 "name" 或 "age" 一样。
JSON.parse解析字符串并创建一个对象。它将"__proto__"视为一个普通的自有属性。o2这个对象拥有一个名为"__proto__"的属性,其值是{b: 2}。
当你调用 merge(o1, o2) 时:
遍历循环检测到了
o2有一个名为"__proto__"的属性。merge函数试图执行类似这样的操作:1
o1["__proto__"]["b"] = o2["__proto__"]["b"];
在 JavaScript 中,访问
o1["__proto__"]实际上访问的是所有对象共享的Object.prototype。代码成功地在
Object.prototype上添加了属性b之后创建的任何对象都会通过原型链访问到这个
b
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题














