原型链污染:JavaScript深层漏洞

作者:Yolo 发布时间: 2026-06-21 阅读量:7

原型链污染:JavaScript深层漏洞

在 Web 安全领域,原型链污染(Prototype Pollution)是一种独特且危险的攻击方式。它利用了 JavaScript 原型继承机制的特性,通过污染内置对象的原型,影响整个应用程序的行为。本文将深入探讨原型链污染的原理、攻击场景和防御方法。

一、JavaScript 原型链基础

1.1 什么是原型链

JavaScript 使用基于原型的继承模型。每个对象都有一个内部属性 [[Prototype]],指向其原型对象。当访问对象的属性时,如果对象本身不存在该属性,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或到达原型链的顶端(null)。

const obj = {};
console.log(obj.toString); // 继承自 Object.prototype

1.2 原型链的关键特性

  • Object.prototype 是所有对象的根原型
  • Array.prototype 是数组的原型
  • 修改原型上的属性会影响所有继承该原型的对象
  • __proto__ 属性可以访问对象的原型(已标准化但不推荐使用)

二、原型链污染的原理

2.1 污染是如何发生的

原型链污染发生在攻击者能够控制对象属性的键名,并且应用程序使用不安全的对象合并或属性赋值操作时。

典型的漏洞代码:

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;
}

// 攻击者控制的输入
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
const user = {};
merge(user, malicious);

// 现在所有对象都有 isAdmin 属性
console.log({}.isAdmin); // true

2.2 为什么 JSON.parse 是关键的攻击向量

JSON.parse 可以创建带有 __proto__ 键的对象,这是普通对象字面量无法做到的:

// 这种方式不会污染原型
const obj1 = {"__proto__": {"evil": true}};
console.log({}.evil); // undefined

// 这种方式可以污染原型
const obj2 = JSON.parse('{"__proto__": {"evil": true}}');
console.log({}.evil); // true

2.3 构造函数污染

除了 __proto__,攻击者还可以利用 constructor.prototype 进行污染:

const payload = JSON.parse('{"constructor": {"prototype": {"isAdmin": true}}}');

三、实际攻击场景

3.1 权限绕过

通过污染 isAdminrole 等属性,攻击者可以绕过权限检查:

// 污染后
if (user.isAdmin) {
    // 执行管理员操作
}

3.2 拒绝服务(DoS)

污染原型上的关键方法可以导致应用程序崩溃:

// 污染 toString 方法
const payload = JSON.parse('{"__proto__": {"toString": "polluted"}}');
// 导致所有对象的 toString 调用失败

3.3 远程代码执行(RCE)

在某些情况下,原型链污染可以导致 RCE:

// 污染 exec 或类似方法
const payload = JSON.parse('{"__proto__": {"shell": "rm -rf /"}}');

3.4 真实案例:jQuery CVE-2019-11358

jQuery 的 $.extend 函数存在原型链污染漏洞:

// 漏洞利用
$.extend(true, {}, JSON.parse('{"__proto__": {"polluted": true}}'));
console.log({}.polluted); // true

四、常见的污染入口

4.1 不安全的对象合并

// 危险的合并函数
function deepMerge(target, source) {
    for (const key in source) {
        if (source[key] && typeof source[key] === 'object') {
            deepMerge(target[key] = target[key] || {}, source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

4.2 URL 参数解析

// 解析查询参数时
const params = new URLSearchParams(location.search);
const obj = {};
for (const [key, value] of params) {
    obj[key] = value; // 如果 key 是 __proto__...
}

4.3 Cookie 解析

// 解析 Cookie
document.cookie.split(';').forEach(cookie => {
    const [key, value] = cookie.trim().split('=');
    config[key] = decodeURIComponent(value);
});

4.4 配置文件解析

// 解析 YAML/JSON 配置文件
const config = yaml.parse(userInput);
Object.assign(defaults, config);

五、防御方法

5.1 使用 Object.create(null)

创建没有原型的对象,从根本上避免污染:

const safeObj = Object.create(null);
safeObj.__proto__ = {evil: true}; // 不会污染 Object.prototype
console.log({}.evil); // undefined

5.2 使用 Map 代替普通对象

const safeMap = new Map();
safeMap.set('__proto__', {evil: true}); // 安全

5.3 安全的对象合并

function safeMerge(target, source) {
    const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
    
    for (const key in source) {
        if (dangerousKeys.includes(key)) continue;
        
        if (source[key] && typeof source[key] === 'object') {
            if (!target[key]) target[key] = {};
            safeMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

5.4 使用 Object.freeze

冻结关键原型,防止被修改:

Object.freeze(Object.prototype);
Object.freeze(Array.prototype);

5.5 使用结构化克隆

// 使用 structuredClone 代替 JSON.parse + 合并
const safeCopy = structuredClone(userInput);

5.6 输入验证

function sanitizeKey(key) {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
        throw new Error('Dangerous key detected');
    }
    return key;
}

六、检测与测试

6.1 静态分析工具

  • eslint-plugin-security:检测不安全的对象操作
  • Semgrep:规则库包含原型链污染检测

6.2 动态测试

// 污染检测函数
function isPrototypePolluted() {
    const testObj = {};
    return testObj.polluted === true;
}

// 测试后清理
delete Object.prototype.polluted;

6.3 模糊测试

使用工具如 jsfuzzfastify-auto-push 对应用程序进行模糊测试,发现潜在的污染点。

七、Node.js 中的特殊风险

7.1 require() 的污染

Node.js 的模块系统依赖对象查找,原型链污染可能影响模块加载:

// 污染后可能导致加载恶意模块
require('child_process').exec('malicious');

7.2 命令执行链

某些库(如 lodash)在特定版本中存在污染导致的 RCE:

// lodash < 4.17.12
_.defaultsDeep({}, JSON.parse('{"__proto__": {"constructor": {"prototype": {"exec": "..."}}}}'));

八、最佳实践总结

  1. 永远不要信任用户输入:对所有输入进行验证和清理
  2. 使用安全的数据结构:优先使用 MapSetObject.create(null)
  3. 冻结原型:在应用启动时冻结 Object.prototype 等关键原型
  4. 使用最新版本的库:及时更新存在已知漏洞的依赖
  5. 代码审查:关注对象合并、属性赋值等操作
  6. 安全测试:将原型链污染纳入安全测试范围

结语

原型链污染是 JavaScript 生态系统中一个深刻的安全问题,它根植于语言的设计特性。理解其原理和防御方法对于构建安全的 Web 应用至关重要。随着 JavaScript 在服务端(Node.js)的广泛应用,这种漏洞的影响范围已经超出了浏览器环境,成为全栈开发者必须关注的安全议题。

在实际开发中,保持警惕、使用安全的数据处理方式、及时更新依赖,是防范原型链污染的关键。记住:安全不是功能,而是基础


本文是网络安全系列文章的一部分,更多内容请关注后续更新。