原型链污染介绍
原型链污染
JavaScript 安全领域的一个术语,指攻击者通过向对象的原型上注入或修改属性,从而影响所有继承该原型的对象,可能导致程序行为异常或安全漏洞。
原型链(Prototype Chain):对象通过
__proto__或Object.getPrototypeOf连接形成的继承链。污染(Pollution):攻击者修改原型上的属性,间接改变所有实例的行为。
原型链
在 JavaScript 中,每个对象都有一个隐藏的属性,通常我们通过 __proto__ 或 Object.getPrototypeOf(obj) 来访问它,这个内部属性指向它的 原型对象(prototype)。
1 | let obj = {}; |
原型链的概念
- 当访问对象的某个属性时,如果对象本身没有该属性,JS 引擎会沿着原型链查找。
- 原型链是通过对象的
[[Prototype]](即__proto__)连接起来的,直到null为止。
1 | test --> prototype --> 原型对象(test 的父类) --> Function --> Object --> null |
test是一个对象或函数。test.__proto__指向它的父类(构造函数或对象)。- 如果
test是函数,则其原型链为:
1 | test (Function) |
null是原型链的尽头,没有更高层的原型。
构造函数
在 JavaScript 中,构造函数(Constructor)是一种用于创建和初始化对象的特殊函数,是实现面向面向对象编程的核心概念之一。它主要用于批量创建多个具有相同结构和行为的对象实例。
构造函数的基本特征
- 命名规范:通常以大写字母开头(如
Person、Car),便于与普通函数区分 - 调用方式:必须使用
new关键字调用,否则会失去构造函数的特性 - 内部机制:
- 自动创建一个新的空对象
this关键字指向这个新对象- 自动返回这个新对象(无需显式
return)
- 作用:为新对象初始化属性和方法
基础用法示例
1 | // 定义构造函数 |
构造函数与普通函数的区别
- 调用方式:构造函数必须用
new调用,普通函数直接调用 - 返回值:构造函数默认返回新对象,普通函数默认返回
undefined this指向:- 构造函数中
this指向新创建的实例 - 普通函数中
this指向调用者(严格模式下可能为undefined)
- 构造函数中
1 | // 作为普通函数调用(不推荐) |
原型
在 JavaScript 中,原型(Prototype) 是实现对象继承和属性共享的核心机制,是理解 JS 面向对象编程的关键概念。它解决了传统面向对象语言中 “类” 的部分功能,让对象可以通过链式关联共享属性和方法。
原型的基本概念
每个 JavaScript 对象(除 null 外)在创建时都会关联一个 “原型对象”,这个原型对象就像一个 “模板”,当前对象可以共享原型对象中的属性和方法。
简单说:
- 当你访问一个对象的属性 / 方法时,JS 会先在对象自身查找;
- 如果找不到,就会去它关联的原型对象中查找;
- 如果原型对象中也没有,就去原型的原型中查找…… 以此类推,直到找到或查找到
null(原型链终点)。
三个核心概念:
prototype、__proto__、constructor
要理解原型,必须理清三个关键属性的关系:
prototype(构造函数的原型属性)
所有函数(包括构造函数)都有一个 prototype 属性,它指向一个对象(即 “原型对象”)。这个原型对象的作用是:当用这个函数作为构造函数(通过 new 调用)创建实例时,实例会自动关联到这个原型对象上,从而共享原型中的属性和方法。
1 | // 定义一个构造函数 |
__proto__(实例对象的原型指针)
所有对象(除 null)都有一个 __proto__ 属性(非标准但被广泛支持,标准方法是 Object.getPrototypeOf()),它指向创建该对象的构造函数的 prototype 属性所指向的原型对象。
简单说:实例的 __proto__ === 构造函数的 prototype。
1 | // 用 Person 构造函数创建实例 |
constructor(原型对象的构造器指针)
原型对象默认有一个 constructor 属性,它指向该原型对象对应的构造函数。
1 | // 原型对象的 constructor 指向构造函数 |
三者关系图
用代码示例的关系可以简化为:
1 | person1(实例) |
原型链(Prototype Chain)
由于每个原型对象本身也是对象,它也有自己的 __proto__ 属性,指向更上层的原型对象。这种 “对象→原型→原型的原型→……→null” 的链式结构,称为原型链。
原型链是 JS 中属性查找的核心机制:当访问一个对象的属性时,JS 会沿原型链逐层查找,直到找到目标或到达 null(返回 undefined)。
示例:原型链查找过程
1 | function Person(name) { |
原型链的终点是 null:Object.prototype.__proto__ === null(所以 null 没有原型)。
原型的核心作用
属性和方法共享
把公共属性 / 方法定义在原型上,所有实例可以共享,避免每个实例都创建一份副本,节省内存。1
2
3
4
5
6
7
8
9
10
11
12
13function Person(name) {
this.name = name; // 每个实例独有的属性
}
// 所有实例共享的方法(定义在原型上)
Person.prototype.getName = function() {
return this.name;
};
const p1 = new Person("Alice");
const p2 = new Person("Bob");
console.log(p1.getName === p2.getName); // true(同一个方法,共享)实现继承
原型链是 JS 实现继承的基础。通过修改构造函数的原型指向另一个对象的实例,可以让该构造函数的实例继承另一个对象的属性和方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 父构造函数
function Animal(type) {
this.type = type;
}
Animal.prototype.eat = function() {
console.log("Eating...");
};
// 子构造函数
function Dog(name) {
this.name = name;
}
// 让 Dog 继承 Animal:将 Dog 的原型指向 Animal 的实例
Dog.prototype = new Animal("dog");
// 修复 constructor 指向(因为修改原型后,constructor 会丢失)
Dog.prototype.constructor = Dog;
// Dog 实例可以访问 Animal 的属性和方法
const dog = new Dog("Buddy");
console.log(dog.type); // "dog"(继承自 Animal)
dog.eat(); // "Eating..."(继承自 Animal.prototype)扩展内置对象
可以通过修改内置对象(如Array、Object)的原型,给所有实例添加新方法1
2
3
4
5
6
7// 给所有数组添加一个求和方法
Array.prototype.sum = function() {
return this.reduce((total, item) => total + item, 0);
};
const arr = [1, 2, 3];
console.log(arr.sum()); // 6
- 原型:强调对象之间的关联关系(“谁是谁的原型”),是一种抽象的机制描述。
- 原型对象:强调这种关联关系中被指向的那个具体对象(“作为原型的那个对象”),是一个实际存在的对象。
如果把 person 比作 “儿子”,Person.prototype 比作 “父亲”,那么
- “原型” 相当于 “父子关系” 这个概念;
- “原型对象” 相当于 “父亲” 这个具体的人。
prototype
prototype 属性是 JavaScript 继承机制的核心基础,它通过以下方式决定继承关系:
- 建立原型关联:构造函数的
prototype指向一个原型对象,当用该构造函数创建实例时,实例的__proto__会指向这个原型对象,形成 “实例→原型对象” 的关联。 - 实现属性方法共享:定义在
prototype上的属性和方法,会被所有实例共享。实例访问属性 / 方法时,若自身没有,会自动到prototype中查找。 - 构建原型链:若将构造函数的
prototype指向另一个构造函数的实例(或其原型),则形成多层关联的原型链。子类实例可通过原型链访问父类prototype中的成员,从而实现继承。
1 | function Parent() {} |
简言之,prototype 通过构建原型关联和原型链,让子类实例能共享父类的属性和方法,这就是 JS 继承的本质。
原型链污染
原型链污染是一种利用 JavaScript 原型链机制(尤其是 prototype 属性的可修改性)产生的安全漏洞,其核心是恶意修改对象的原型(prototype),导致所有继承自该原型的实例都受到影响。
原理:prototype 的可修改性与原型链共享特性
JavaScript 中,prototype 指向的原型对象是可动态修改的。当一个对象的原型(通过 __proto__ 或构造函数的 prototype)被篡改时,所有继承自该原型的实例都会 “继承” 这些篡改后的属性或方法 —— 这就是原型链污染的根源。
例如,Object.prototype 是所有对象的根原型,如果恶意修改它:
1 | // 污染 Object 的原型 |
典型场景:通过不可信数据修改原型
原型链污染常发生在程序处理不可信数据(如用户输入、网络请求)时,未过滤对 __proto__ 或 prototype 的操作。例如:
1 | // 模拟一个合并对象的函数(常见于配置合并、数据处理) |
核心关系:prototype 是污染的 “目标”
原型链污染的本质是通过篡改 prototype 指向的原型对象,利用原型链的共享特性影响所有实例。prototype 作为原型对象的引用入口,其可修改性使得这种污染成为可能。
简言之,原型链污染是对 prototype 可修改性的恶意利用,而理解 prototype 与原型链的机制是识别和防御这种漏洞的关键。
merge和clone
Merge(合并)
就是把多个对象或数组的属性/元素合在一起。
Clone(拷贝)
就是创建一个对象/数组的副本。
merge 的风险
- 作用:合并多个对象时,会递归遍历属性并赋值到目标对象。
- 漏洞点:如果递归时不检测属性名是否是
__proto__、prototype、constructor,就可能写入到原型上。 - 结果:攻击者发送的 JSON payload 就能直接污染全局对象的原型链。
示例(Lodash < 4.17.5 的 _.merge 漏洞):
1 | const _ = require('lodash'); |
clone 的风险
- 浅拷贝(shallow clone):一般风险较低,因为只是复制引用。
- 深拷贝(deep clone):如果实现逻辑和 merge 类似(递归复制属性),同样会遍历原型链上的危险属性名。
- 危险场景:自定义 deepClone 函数、或者使用存在漏洞的
cloneDeep实现时,可能在克隆过程中意外把__proto__属性赋给新对象,从而污染原型。
例子(早期的 clone-deep 库漏洞):
1 | const cloneDeep = require('clone-deep'); |
简单的例子
1 | function merge(target, source) { |