Nodejs原型链污染
单独提出来是方便自己下次看到能快速浏览,之前的笔记太混乱了,链路逻辑有点混乱,一些细节也忽略掉了,借此重构一下;再者是自己本身对 nodejs 这一块的认识没有那么深刻
JavaScript 原型链
继承属性
JavaScript 对象有一条指向原型的链,当访问其属性时除了该对象,还会在其原型甚至是原型的原型中去查找该属性,直到到达原型链的末尾
想要访问一个对象的原型有两种方法,函数标准形式:通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 来访问和修改;非标准形式:JavaScript 访问器 __proto__
1 | let o ={} |
根据 ECMAScript 标准,符号 someObject.[[Prototype]] 用于指定 someObject 的原型
这不是代码中可写的语法,而是规范中指定一个对象的记法

对于以下例子,我们显式设置 o 的原型为 {b, 2}
1 | let o ={ |
对于这个对象字面量,a 键是自有属性,可以将 [[Prototype]] 理解为对象的一个“内部属性”,这里显然是通过 o 的原型找到了 b 属性
注意这里不管嵌套多少 __proto__,其原型链都为 {a, 1} -> {b, 2} -> ... -> [Object: null prototype] {} -> null
构造函数
构造函数的功能之一是复用,尤其是对于让多个实例共享方法
构造函数是使用 new 调用的函数,在使用 new 运算符调用函数时,构造函数的 prototype 属性将成为新实例对象的原型,其意义就是这个,避免每个实例都重复创建同一份函数
1 | function O(value) { |

Constructor.prototype 默认具有一个自有属性:constructor,返回一个引用,指向创建该实例对象的构造函数
1 | function O(value) { |
可以和前文继承属性这一块结合起来理解,实例 o1 本身是没有 constructor 自有属性的,既然找不到只能去它的原型也就是构造函数上面去找
字面量的隐式构造函数
在 JavaScript 中,这些字面量语法会创建隐式设置原型([[Prototype]])的实例
这里其实很容易理解,因为关键字的底层就是这些构造函数
1 | const o = {a: 1} // 本来是 const o = new Object({ a: 1 }); |
总结一下:
- 实例继承了(构造函数)
Constructor.prototype Array.prototype、String.prototype、Function.prototype等继承了Object.prototypeObject.prototype继承了null
原型链污染原理
理解了 JavaScript 原型链,再看污染就好多了;因为 JavaScript 代码的接触面相对比较少,所以以前每次都要回顾自己的旧笔记,也是这一次在某比赛中遇到大量 nodejs 的题目,才这样系统性的梳理一遍
1 | let o1 = {a: 1} |
现在应该理解了,他俩的原型都是 Object.prototype,这里我们直接给原型一个属性 b 并赋值 2
虽然 o1 o2 的自有属性中都没有 b 键,但是他们的原型却有,所以依然输出 2
而一般的原型链污染都会伴随错误的 merge 合并操作
1 | function merge(target, source) { |


merge 函数的核心作用就是去遍历键名进行合并操作,这里显然 __proto__ 也是一个键,自然而然将 Object.prototype 也给污染了,那么所有继承自它的对象都会平白无故的有一个属性 b
这里 JSON.parse 是为了将原本的对象字面量 __proto__ 解析成真正的键名,否则是无法污染的;如果没有那么只会给 o2 实例增添一个原型 {"b", 2},当作设置对象原型的语法处理,并没有影响到 Object.prototype,我们可以从继承属性的例子得知这个结果


同样的,实例的 constructor 指向创建该实例对象的构造函数,我们可以通过 constructor.prototype 来访问其构造函数的原型达到污染的目的
1 | let o1 = {}; |
顺便提一句,console.log(o1.constructor.prototype.constructor); 又会循环回去到 o1 的构造函数 Object
1 | let o1 = {}; |







