先了解一下原型的定义
在javascript中,原型是一个对象,其他的对象可以通过原型实现属性和方法的继承,每一个js对象(除了null)都有一个内置的[[Prototype]]属性(在大多数浏览器中可以通过__proto__
访问),这个属性指向他的原型对象
这里顺便也介绍一下,javascript对于对象自身的查找机制:
首先,会在对象自身查找该属性
如果找不到,则在其原型对象上查找
继续沿着原型链向上查找,知道找到该属性或者到达原型链末端(null)
简单举个例子:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
const john = new Person('John');
// 原型链:
// john -> Person.prototype -> Object.prototype -> null
prototype
&&__proto__
__proto__
属性
首先,其是一个非标准的属性,他提供了对对象内部[[Prototype]]属性的访问,即我们可以直接获取或设置一个对象的原型对象
prototype
prototype
是一个属性,每个对象都有,但是确切来说,所有的对象都有一个原型(prototype
),这是一个内置的属性,它指向该对象的构造函数的原型对象。每当你创建一个新对象时,这个对象都会继承其构造函数的原型对象中的属性和方法
注:只有构造函数(即函数类型的对象)才有 prototype
属性
js是一种基于原型的编程语言,其没有传统的类继承机制,但是可以通过prototype实现类似的继承效果,允许对象继承其他对象的属性和方法
在js当中我们要定义一个类时,就需要定义构造函数的形式去定义,先上一个最简单的
function Foo(){
this.bar = 1
}
new Foo()
其中this.bar就是Foo类中的一个属性,我们也可以定义方法在其中
function Foo() {
// 给新创建的实例添加一个属性 bar 并赋值为 1
this.bar = 1;
// 给新创建的实例添加一个方法 show,用于打印 bar 属性的值
this.show = function () {
console.log(this.bar);
};
}
// 创建 Foo 的一个实例并调用 show 方法
(new Foo()).show();
控制台打印出来是1
,其实这样定义这个方法是绑定到对象身上的,而不是在原型内部的
function Foo(){
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()
可以使用prototype理解为能向对象中添加属性和方法,相当于实例化后的Foo类都能包含所有的属性和方法,不用一个个去定义了
foo.__proto__ = Foo.prototype
//一个对象的__proto__属性,指向这个对象所在类的prototype属性
所以就可以使用__proto__
去访问Foo的原型,也就做到了继承
原型链污染
function Father(){
this.first_name = "Donald"
this.last_name = "Trump"
}
function Son(){
this.first_name = "Melania"
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
这样就做到了继承了,Son继承了father的所有属性和方法,然后根据js的寻找机制,若找不到就去 Son.__proto__
中找,找不到就会一级一级的继承去找,这边我们将它和 father 做了继承,所以会找到 last_name 并输出,但是要注意的是,first_name 会输出 son 里面的,因为构造函数的调用顺序,在创建 Son 实例时,会覆盖掉从 Father 继承来的同名的属性
还有一个特点,就是,对于原型链继承,会让所有的Son实例共享Father实例的属性,如果修改了某个Son实例的原型属性,也会影响到其他的Son实例
举个例子:
function Father() {
this.first_name = "Donald";
this.last_name = "Trump";
}
function Son() {
this.first_name = "Melania";
}
// 设置 Son 的原型为 Father 的一个实例
Son.prototype = new Father();
// 创建两个 Son 实例
let son1 = new Son();
let son2 = new Son();
// 修改 son1 的原型属性(这里以修改 last_name 为例)
son1.__proto__.last_name = "NewLastname";
// 查看 son2 的 last_name 属性
console.log(son2.last_name); // 输出: NewLastname
修改了Son1的原型属性,就会影响Son2的属性
所以,原型链污染就是利用的这个特点,通过修改原型属性,就会影响其他的类
关键也是要知道js的查找机制
同时对于`__proto__`和`prototype`的区别
`prototype` 属性是函数所独有的,而 `__proto__` 属性是每个对象都有的。
`prototype` 属性指向一个对象,它是用来存储属性和方法,这些属性和方法可以被该函数的实例对象所继承。而 `__proto__` 属性指向该对象的原型,它是用来实现对象之间的继承
dome
// 定义一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 在 Person 的原型上定义一个方法
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name + '.');
}
// 创建一个 Person 实例对象
var person = new Person('Alice', 20);
// 输出实例对象的属性和方法
console.log(person.name); // "Alice"
console.log(person.age); // 20
person.sayHello(); // "Hello, my name is Alice."
// 输出 Person 和实例对象的 __proto__ 属性
console.log(Person.__proto__); // [Function]
console.log(person.__proto__); // Person { sayHello: [Function] }
// 输出 Person 和实例对象的 prototype 属性
console.log(Person.prototype); // Person { sayHello: [Function] }
console.log(person.prototype); // undefined 实例对象没有prototype属性,只有构造函数有,所以返回undefined
那么什么情况下才会存在污染呢?
污染中常见的危险函数
merge()
在javascript中,merge()并不是一个内置函数,当时通常指用于合并对象或数组操作
比如:
function merge(...objects) {
return Object.assign({}, ...objects);
}
// 使用示例
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = merge(obj1, obj2); // { a: 1, b: 3, c: 4 }
clone()
在 JavaScript 中,clone() 不是原生提供的函数,但通常指创建一个对象的独立副本(克隆)的操作。由于 JavaScript 中对象和数组是通过引用传递的,简单的赋值不会创建真正的副本,因此需要专门的克隆方法
对于对象,比如:
// 方法1:使用 Object.assign()
function clone(obj) {
return Object.assign({}, obj);
}
// 方法2:使用展开运算符
function clone(obj) {
return { ...obj };
}
写一个dome
function merge(target,source){
for(let key in source){
if (key in source && key in target){
merge(target[key],source[key])
}else{
target[key] = source[key]
}
}
}
let o1 = {}
let o2 = {a: 1, "__proto__": {b:2}}
merge(o1,o2)
console.log(o1.a,o2.b)
o3 = {}
console.log(o3.b)
merge
该函数用于递归合并两个对象。如果 source
和 target
有相同属性名且对应值都是非 null
对象,则递归合并这两个子对象;否则直接将 source
的属性值赋给 target
对应属性
o1
是一个空对象。
o2
是一个具有属性 a
值为 1
的对象,同时通过 __proto__
显式设置其原型对象,原型对象包含属性 b
,值为 2
merge
函数开始遍历 o2
的可枚举属性
o2
有两个可枚举属性:a
和 __proto__
对于属性 a
,o1
中没有该属性,所以 o1.a
会被赋值为 1
对于属性 __proto__
,它也是一个可枚举属性(虽然 __proto__
用于设置原型,但在这里作为普通属性被遍历),o1
中没有 __proto__
属性,所以 o1.__proto__
会被赋值为 {b: 2}
,也就是 o1
的原型被修改为包含属性 b
的对象
o1.a
:由于 merge
操作将 o2.a
的值 1
赋给了 o1.a
,所以 o1.a
输出为 1
o2.b
:b
是 o2
原型对象上的属性,通过原型链查找可以访问到,所以 o2.b
输出为 2
o3
是一个新创建的空对象,它没有经过任何原型修改操作,其原型是默认的 Object.prototype
,不包含属性 b
所以 o3.b
输出为 undefined
合并了但是并没有污染
因为,我们创建的o2的过程中,__proto__
代表的o2的原型,此时遍历o2的所有的键名,拿到的事[a,b],__proto__
并不是key,所以没有修改object的原型
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
if (typeof source[key] === 'object' && source[key]!== null && typeof target[key] === 'object' && target[key]!== null) {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
} else {
target[key] = source[key];
}
}
}
let o1 = {};
let o2 = { a: 1 };
// 直接修改 o2 的原型
Object.setPrototypeOf(o2, { b: 2 });
merge(o1, o2);
console.log(o1.a, o1.b);
let o3 = {};
console.log(o3.b);
o3 这样也就有了 b 属性,说明 Object 已经被污染
所以是直接影响到了 object,所以 o3 才会有 b2 的属性
总结一下可能存在的场景
不安全的对象合并/复制
function unsafeMerge(target, source) {
for (let key in source) {
target[key] = source[key]; // 可能覆盖 __proto__ 或 constructor
}
}
const payload = { "__proto__": { isAdmin: true } };
unsafeMerge({}, payload);
console.log({}.isAdmin); // true(污染成功)
存在可以直接修改__proto__
或 constructor.prototype
使用有漏洞的第三方库(间接导致污染)
Lodash (CVE-2019-10744):_.merge、_.defaultsDeep 曾存在漏洞
jQuery (CVE-2019-11358):$.extend(true, {}, payload) 可能被利用
Node.js 库(如 hoek、handlebars 等)
还有可能就是动态属性名赋值,比如可能赋值__proto__