JS原型链污染
nodejs和javascript语法和标准有些不同,应用场景不一样,但是在原型链污染这方面可以互通。
JS创建对象的三种方法
1 | js创建对象的三种方法 : |
原型链
原型
javascript里的每一个对象(object),都有一些内置属性(property),原型本身也是一个对象,所以他也有自己的原型,这就形成了一个原型链,当一个原型的原型是null,原型链就会在这里结束。
这个”原型“属性是一个指向对象原型的一个对象,这个属性没有统一的称呼,但是浏览器都支持使用 ”__proto__“来查看原型。访问一个对象原型的标准方法是使用 ”Object.getPrototypeOf()“。
Prototype 原型 | 原型对象
1、Prototype它是【函数的】一个属性
2、Protopype是个对象
3、当我们创建函数的时候会默认添加Prototype这个属性
__proto__ 隐式原型
1、【对象】的属性
2、指向构造函数的Protopype
3、obj._proto_ === test.prototype 等于true
原型链顶层
Object.prototype.__proto__ 等于null
原型链查找
instanceof运算符可以用来判断某个构造函数的prototype属性是否存在另外一个要检测对象的原型链
1 | function My(){} |
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
示例
1 | function test (name) { |
结构剖析
1 | obj { |
链子剖析
1 | obj -> __proto__ -> test.prototype -> __proto__ -> Object.prototype -> __proto__ -> null |
这就是obj的原型链
原型链污染
因为原型链就是套娃的,一级接着一级,属性是从上面往下叠加的,下面的都会继承上面的属性,如果我们在原型链的上级添加了恶意对象,那么原型链就会被污染,后续创建的对象都可以继承并调用恶意对象。
如果服务端使用某些可以复制、继承变量的函数,函数的参数是用户可控,那么就有可能造成原型链污染攻击。
此时b的上级原型a的属性已经被污染,b继承a后,也可以调用恶意对象。
原型链污染利用
在javascript中,function是一个用于定义函数的关键字,Function是代表所有函数的内置原型对象。
constructor概念
Object.prototype.constructor其中constructor是一个对象的数据属性,创建对象后,访问constructor属性,可以返回构造该对象的来源【就是告诉你它是从哪来的】(不是该对象的原型链上级,两者不同)
1 | demo: |
constructor
和 prototype
的角色
constructor
的作用
每个 JavaScript 对象都有一个
constructor
属性,默认情况下,它指向创建该对象的构造函数。例如:
1
2const obj = {};
console.log(obj.constructor === Object); // true
prototype
的作用
构造函数的
prototype
属性是实例对象的默认原型(即实例的__proto__
)。例如:
1
2
3function MyClass() {}
const obj = new MyClass();
console.log(obj.__proto__ === MyClass.prototype); // true
两者的关系
- 修改构造函数的
prototype
会影响由该构造函数创建的所有对象的原型。 - 通过
constructor.prototype
可以间接修改对象的原型链,甚至是全局对象的原型。
所以,当我们的__proto__被过滤了,我们可以直接用constructor.prototype绕过
1 | function merge(target, source) { |
Function概念
每一个javascript的function实际上都是Function对象,Function是javascript内置的对象,Function用以实现很多基本的功能,如Number、toString等。
1 | (function () {}).constructor |
Function() constructor
Function()构造器可以创建一个Function对象,可以直接调用Function()构造器动态的创建函数。但是会存在像eval()的安全隐患和一些性能问题。
eval()和Function区别:
1、eval()可以访问本地的变量、全局变量
2、Funtion()创建函数时只能执行全局变量
JSON.parse
看一个经典的递归合并函数
1 | function merge(target,source) { |
核心思想是理解 __proto__
在对象定义中的行为,以及它在合并操作中如何影响目标对象。以下是逐步解释:
1. __proto__
的行为
在 JavaScript 中,__proto__
是一个特殊属性,用于设置对象的原型。它不是普通的键,而是与对象的原型链相关。
当 __proto__
被用作键:
1 | let o2 = {a: 1, "__proto__": {b: 2}}; |
- 在这种情况下,
__proto__
表面上看是一个普通的键值对。 - 实际行为:
__proto__
这个键会被特殊处理,将{b: 2}
作为o2
的原型,而不是普通的对象属性。
相当于:
1 | o2.a = 1; |
结果:
o2.a
是 o2 自己的属性。o2.b
不直接存在于 o2,而是来自于它的原型{b: 2}
。
如果直接访问 o2.b
,JavaScript 会通过原型链查找,因此 o2.b
的值是 2
。
2. 合并函数的行为
我们来看 merge
函数:
1 | function merge(target, source) { |
for...in
循环:
遍历source
的所有可枚举属性,包括继承的属性(从原型链上来的)。key in source
判断: 确保key
是source
中的一个属性(包括自身和原型链)。
3. 为什么只有 a
被合并?
当你执行:
1 | let o1 = {}; |
关键点:
for...in
不会直接枚举__proto__
作为普通键。o2
只有一个直接属性a
,因此merge
函数中只会处理a
。__proto__
被用作原型设置,{b: 2}
成为了o2
的原型。
因此:
o1
合并结果:o1.a = 1
。o1.b
是 undefined:因为b
是o2
的原型属性,而merge
函数不会递归或复制原型链的属性。
4. 为什么 o1.b = 2
这句话不成立?
合并后,o1
并没有 b
,因为:
b
并不是o2
自身的属性,而是其原型的属性。merge
函数只处理直接属性,未复制b
到o1
。
总结: __proto__
在对象字面量中会被解释为设置原型,而不是普通键值对。所以,merge
只会处理 o2
的直接属性(a
),不会处理通过原型链继承的 b
,因此 o1.b
仍然是 undefined
。
5.为什么运行结果确是2
运行结果来看,console.log(o1.b)
返回了 2
,这表明在某种程度上确实读取到了属性 b
,而这是从 __proto__
原型链继承的属性。我们重新分析以下几个关键点,帮助理解为什么会发生这种现象:
5.1. __proto__
在对象字面量中的作用
当你定义:
1 | let o2 = { a: 1, "__proto__": { b: 2 } }; |
o2
的结构变成:
1 | o2 = { |
这意味着 o2
的原型被设置为 { b: 2 }
,即 o2
通过原型链可以访问 b
。
5.2. merge
函数如何工作
1 | function merge(target, source) { |
for...in
的行为:for...in
会迭代source
的可枚举属性,包括继承自原型的属性。
在这个情况下:
o2
的直接属性是a
。o2
通过原型链继承了属性b
,且b
是可枚举的。
5.3. 执行过程
当 merge(o1, o2)
执行时:
key = "a"
:将o2.a
的值1
合并到o1
。key = "b"
:由于b
是从__proto__
继承的,它也被for...in
迭代到,因此o2.b
的值2
被赋值到o1.b
。
最终,o1
的内容是:
1 | o1 = { |
5.4. 为什么 console.log(o1.b)
输出 2
?
当 merge
函数运行完后,o1
对象被赋予了一个新属性 b
,其值为 2
。因此,当你执行:
1 | console.log(o1.b); // 输出 2 |
这正是由于 b
被直接合并到 o1
的原因。
6. 这是否算原型污染?
不算真正的原型污染,原因如下:
- 污染的定义:原型污染意味着修改了原型对象(如
Object.prototype
),导致所有对象都受到影响。 - 你的操作仅仅是在
o1
上设置了一个普通属性b
,并没有修改全局原型链。因此,其他对象(如{}
)并不会受到影响。
7. 验证是否污染了全局原型
你可以验证:
1 | console.log({}.b); // 如果是原型污染,会输出 2,但是输出的是undefined |
若结果为 undefined
,说明没有污染全局原型。
结论
你当前的 merge
操作仅仅是将 b
合并到了 o1
对象上,没有导致原型污染。 要实现原型污染,需要明确操作全局原型链(例如通过修改 Object.prototype
)。
好,那么我们言归正传,如何才能真正污染呢?
主角登场–请看下面代码
1 | function merge(target, source) { |
1. JSON.parse
和 __proto__
的作用
在 JSON.parse
中,{"__proto__": {"b": 2}}
会被解析为:
1 | { |
此时,o2
的 __proto__
实际上是一个普通属性,而不是原型链。
但是,当 merge
函数执行以下语句时:
1 | target[key] = source[key]; |
那么如果 key
是 __proto__
,它会触发 原型链修改,这样就相当于给最顶层的Object.prototype所指向的对象添加了属性,所以我们随便创建一个对象也就有了b这个属性 。
2. 原型污染的发生
在 merge(o1, o2)
中:
key = "a"
:o1.a = 1
,无问题。key = "__proto__"
:o1["__proto__"] = { b: 2 }
。
当设置 o1["__proto__"] = { b: 2 }
时,实际上修改了 o1
的原型链,使 o1
和所有继承自 Object.prototype
的对象(包括 o3
)的原型被污染。
因此:
o1.b
等于2
,因为b
来自于被污染的原型。o3.b
也等于2
,即使o3
本身没有b
,它仍然从全局的Object.prototype
继承了污染。
3. 验证污染
可以通过以下方式验证污染:
1 | console.log({}.b); // 输出 2,说明 Object.prototype 被污染 |
这就是JS原型链污染基础了。