原型链污染

JavaScript 安全领域的一个术语,指攻击者通过向对象的原型上注入或修改属性,从而影响所有继承该原型的对象,可能导致程序行为异常或安全漏洞。

  • 原型链(Prototype Chain):对象通过 __proto__Object.getPrototypeOf 连接形成的继承链。

  • 污染(Pollution):攻击者修改原型上的属性,间接改变所有实例的行为。

原型链

在 JavaScript 中,每个对象都有一个隐藏的属性,通常我们通过 __proto__Object.getPrototypeOf(obj) 来访问它,这个内部属性指向它的 原型对象(prototype)。

1
2
let obj = {};
console.log(obj.__proto__ === Object.prototype); // true

原型链的概念

  • 当访问对象的某个属性时,如果对象本身没有该属性,JS 引擎会沿着原型链查找。
  • 原型链是通过对象的 [[Prototype]](即 __proto__)连接起来的,直到 null 为止。
1
test --> prototype --> 原型对象(test 的父类) --> Function --> Object --> null
  • test 是一个对象或函数。
  • test.__proto__ 指向它的父类(构造函数或对象)。
  • 如果 test 是函数,则其原型链为:
1
2
3
4
5
6
7
test (Function)
↓ __proto__
Function.prototype
↓ __proto__
Object.prototype
↓ __proto__
null
  • null 是原型链的尽头,没有更高层的原型。

构造函数

在 JavaScript 中,构造函数(Constructor)是一种用于创建和初始化对象的特殊函数,是实现面向面向对象编程的核心概念之一。它主要用于批量创建多个具有相同结构和行为的对象实例。

构造函数的基本特征

  1. 命名规范:通常以大写字母开头(如 PersonCar),便于与普通函数区分
  2. 调用方式:必须使用 new 关键字调用,否则会失去构造函数的特性
  3. 内部机制:
    • 自动创建一个新的空对象
    • this 关键字指向这个新对象
    • 自动返回这个新对象(无需显式 return
  4. 作用:为新对象初始化属性和方法

基础用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义构造函数
function Person(name, age) {
// 初始化属性(this 指向新创建的对象)
this.name = name;
this.age = age;

// 定义方法
this.sayHello = function() {
console.log(`Hello, I'm ${this.name}, ${this.age} years old`);
};
}

// 使用 new 关键字创建实例
const person1 = new Person("Alice", 28);
const person2 = new Person("Bob", 32);

// 访问实例的属性和方法
console.log(person1.name); // "Alice"
person2.sayHello(); // "Hello, I'm Bob, 32 years old"

// 检查实例类型
console.log(person1 instanceof Person); // true

构造函数与普通函数的区别

  1. 调用方式:构造函数必须用 new 调用,普通函数直接调用
  2. 返回值:构造函数默认返回新对象,普通函数默认返回 undefined
  3. this 指向:
    • 构造函数中 this 指向新创建的实例
    • 普通函数中 this 指向调用者(严格模式下可能为 undefined
1
2
3
4
// 作为普通函数调用(不推荐)
const notInstance = Person("Charlie", 25);
console.log(notInstance); // undefined(没有返回值)
console.log(globalThis.name); // "Charlie"(this 指向全局对象)

原型

在 JavaScript 中,原型(Prototype) 是实现对象继承和属性共享的核心机制,是理解 JS 面向对象编程的关键概念。它解决了传统面向对象语言中 “类” 的部分功能,让对象可以通过链式关联共享属性和方法。

原型的基本概念

每个 JavaScript 对象(除 null 外)在创建时都会关联一个 “原型对象”,这个原型对象就像一个 “模板”,当前对象可以共享原型对象中的属性和方法

简单说:

  • 当你访问一个对象的属性 / 方法时,JS 会先在对象自身查找;
  • 如果找不到,就会去它关联的原型对象中查找;
  • 如果原型对象中也没有,就去原型的原型中查找…… 以此类推,直到找到或查找到 null(原型链终点)。

三个核心概念:prototype__proto__constructor

要理解原型,必须理清三个关键属性的关系:

  1. prototype(构造函数的原型属性)

所有函数(包括构造函数)都有一个 prototype 属性,它指向一个对象(即 “原型对象”)。这个原型对象的作用是:当用这个函数作为构造函数(通过 new 调用)创建实例时,实例会自动关联到这个原型对象上,从而共享原型中的属性和方法。

1
2
3
4
5
6
7
// 定义一个构造函数
function Person(name) {
this.name = name; // 实例自身的属性
}

// 构造函数的 prototype 属性指向原型对象
console.log(Person.prototype); // { constructor: Person, ... }
  1. __proto__(实例对象的原型指针)

所有对象(除 null)都有一个 __proto__ 属性(非标准但被广泛支持,标准方法是 Object.getPrototypeOf()),它指向创建该对象的构造函数的 prototype 属性所指向的原型对象。

简单说:实例的 __proto__ === 构造函数的 prototype

1
2
3
4
5
// 用 Person 构造函数创建实例
const person1 = new Person("Alice");

// 实例的 __proto__ 指向构造函数的 prototype
console.log(person1.__proto__ === Person.prototype); // true
  1. constructor(原型对象的构造器指针)

原型对象默认有一个 constructor 属性,它指向该原型对象对应的构造函数。

1
2
3
4
5
// 原型对象的 constructor 指向构造函数
console.log(Person.prototype.constructor === Person); // true

// 实例可以通过原型链访问到 constructor
console.log(person1.constructor === Person); // true(通过 person1.__proto__ 查找)

三者关系图

用代码示例的关系可以简化为:

1
2
3
4
5
person1(实例)
|
__proto__ → Person.prototype(原型对象)
|
constructor → Person(构造函数)

原型链(Prototype Chain)

由于每个原型对象本身也是对象,它也有自己的 __proto__ 属性,指向更上层的原型对象。这种 “对象→原型→原型的原型→……→null” 的链式结构,称为原型链

原型链是 JS 中属性查找的核心机制:当访问一个对象的属性时,JS 会沿原型链逐层查找,直到找到目标或到达 null(返回 undefined)。

示例:原型链查找过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
}

// 给 Person 原型添加方法
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};

const person1 = new Person("Bob");

// 1. 访问 person1.name → 自身有,直接返回 "Bob"
// 2. 访问 person1.sayHello() → 自身没有,去 person1.__proto__(即 Person.prototype)中找,找到并调用
// 3. 访问 person1.toString() → 自身和 Person.prototype 都没有,去 Person.prototype.__proto__(即 Object.prototype)中找(Object 是所有对象的根原型),找到并调用
console.log(person1.toString()); // "[object Object]"(来自 Object.prototype)

// 4. 访问 person1.xxx → 整条原型链都没有,返回 undefined

原型链的终点是 null
Object.prototype.__proto__ === null(所以 null 没有原型)。

原型的核心作用

  1. 属性和方法共享
    把公共属性 / 方法定义在原型上,所有实例可以共享,避免每个实例都创建一份副本,节省内存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function 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(同一个方法,共享)
  2. 实现继承
    原型链是 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)
  3. 扩展内置对象
    可以通过修改内置对象(如 ArrayObject)的原型,给所有实例添加新方法

    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 继承机制的核心基础,它通过以下方式决定继承关系:

  1. 建立原型关联:构造函数的 prototype 指向一个原型对象,当用该构造函数创建实例时,实例的 __proto__ 会指向这个原型对象,形成 “实例→原型对象” 的关联。
  2. 实现属性方法共享:定义在 prototype 上的属性和方法,会被所有实例共享。实例访问属性 / 方法时,若自身没有,会自动到 prototype 中查找。
  3. 构建原型链:若将构造函数的 prototype 指向另一个构造函数的实例(或其原型),则形成多层关联的原型链。子类实例可通过原型链访问父类 prototype 中的成员,从而实现继承。
1
2
3
4
5
6
7
8
9
function Parent() {}
Parent.prototype.parentMethod = function() {};

function Child() {}
// 让 Child 继承 Parent:将 Child 的 prototype 指向 Parent 实例
Child.prototype = new Parent();

const child = new Child();
child.parentMethod(); // 可访问父类原型方法(通过原型链)

简言之,prototype 通过构建原型关联和原型链,让子类实例能共享父类的属性和方法,这就是 JS 继承的本质。

原型链污染

原型链污染是一种利用 JavaScript 原型链机制(尤其是 prototype 属性的可修改性)产生的安全漏洞,其核心是恶意修改对象的原型(prototype),导致所有继承自该原型的实例都受到影响

原理:prototype 的可修改性与原型链共享特性

JavaScript 中,prototype 指向的原型对象是可动态修改的。当一个对象的原型(通过 __proto__ 或构造函数的 prototype)被篡改时,所有继承自该原型的实例都会 “继承” 这些篡改后的属性或方法 —— 这就是原型链污染的根源。

例如,Object.prototype 是所有对象的根原型,如果恶意修改它:

1
2
3
4
5
6
7
8
// 污染 Object 的原型
Object.prototype.evil = "恶意值";

// 所有对象都会继承这个属性
const obj1 = {};
const obj2 = new Array();
console.log(obj1.evil); // "恶意值"(本不该有这个属性)
console.log(obj2.evil); // "恶意值"

典型场景:通过不可信数据修改原型

原型链污染常发生在程序处理不可信数据(如用户输入、网络请求)时,未过滤对 __proto__prototype 的操作。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 模拟一个合并对象的函数(常见于配置合并、数据处理)
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}

// 正常使用:合并普通数据
const config = { port: 8080 };
const userInput = { name: "test" };
merge(config, userInput);
console.log(config); // { port: 8080, name: "test" } (正常)

// 恶意输入:通过 __proto__ 污染原型
const maliciousInput = {
__proto__: { isAdmin: true } // 试图修改原型
};
merge({}, maliciousInput); // 合并时会修改 Object.prototype

// 后果:所有对象都继承了 isAdmin 属性
const obj = {};
console.log(obj.isAdmin); // true(本不该有,污染成功)

核心关系:prototype 是污染的 “目标”

原型链污染的本质是通过篡改 prototype 指向的原型对象,利用原型链的共享特性影响所有实例。prototype 作为原型对象的引用入口,其可修改性使得这种污染成为可能。

简言之,原型链污染是对 prototype 可修改性的恶意利用,而理解 prototype 与原型链的机制是识别和防御这种漏洞的关键。

merge和clone

  1. Merge(合并)

    就是把多个对象或数组的属性/元素合在一起

  2. Clone(拷贝)

    就是创建一个对象/数组的副本

merge 的风险

  • 作用:合并多个对象时,会递归遍历属性并赋值到目标对象。
  • 漏洞点:如果递归时不检测属性名是否是 __proto__prototypeconstructor,就可能写入到原型上。
  • 结果:攻击者发送的 JSON payload 就能直接污染全局对象的原型链。

示例(Lodash < 4.17.5 的 _.merge 漏洞):

1
2
3
4
5
6
const _ = require('lodash');

let obj = {};
_.merge({}, JSON.parse('{"__proto__": {"polluted": "yes"}}'));

console.log({}.polluted); // yes -> 原型链已被污染

clone 的风险

  • 浅拷贝(shallow clone):一般风险较低,因为只是复制引用。
  • 深拷贝(deep clone):如果实现逻辑和 merge 类似(递归复制属性),同样会遍历原型链上的危险属性名。
  • 危险场景:自定义 deepClone 函数、或者使用存在漏洞的 cloneDeep 实现时,可能在克隆过程中意外把 __proto__ 属性赋给新对象,从而污染原型。

例子(早期的 clone-deep 库漏洞):

1
2
3
4
5
const cloneDeep = require('clone-deep');

cloneDeep(JSON.parse('{"__proto__": {"polluted": "true"}}'));

console.log({}.polluted); // "true"

简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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] // 直接赋值
}
}
}

// 调用示例:合并嵌套对象
const target = { a: { x: 1 } };
const source = { a: { y: 2 }, b: 3 };
merge(target, source);