解释 JavaScript 中的 Prototype Pollution (原型污染) 漏洞及其防御。

大家好,欢迎来到今天的“JavaScript Prototype Pollution 防御指南”讲座。 今天我们来聊聊一个JavaScript世界里让人头疼但又不得不面对的家伙:Prototype Pollution(原型污染)。 想象一下,你精心布置的房间(你的 JavaScript 代码),突然被熊孩子(恶意代码)偷偷摸摸地把东西乱放,搞得一团糟,这感觉是不是很糟糕?原型污染就是类似的事情,它能悄无声息地改变你的对象,甚至全局对象,导致意想不到的 Bug,甚至安全问题。

准备好了吗?让我们开始这场和原型污染的斗智斗勇吧!

一、什么是 Prototype Pollution?

简单来说,Prototype Pollution 就是攻击者通过某种方式修改了 JavaScript 对象的原型(prototype)。 由于 JavaScript 的原型继承机制,对原型上的属性的修改会影响到所有基于该原型创建的对象。

记住这句话:“改了原型,影响全家。”

举个栗子:

// 默认情况下,所有的对象都继承自 Object.prototype
console.log({}.toString); // 输出:[Function: toString]

// 现在,让我们来搞点事情,修改 Object.prototype
Object.prototype.isAdmin = true;

// 看看发生了什么
const user = {};
console.log(user.isAdmin); // 输出:true  WTF?!

const anotherUser = {};
console.log(anotherUser.isAdmin); // 输出:true  WTF?!

看到了吗?我们仅仅修改了 Object.prototype,就影响了所有基于 Object 创建的对象。 这就是原型污染的威力,也是它危险的地方。

二、原型污染的危害

  • 意外的 Bug: 你的代码可能突然出现意想不到的行为,因为对象的属性被篡改了。
  • 安全漏洞: 攻击者可能利用原型污染来绕过安全检查,执行恶意代码,甚至窃取数据。
  • 拒绝服务: 攻击者可以修改原型上的方法,导致程序崩溃或无法正常运行。

三、原型污染的常见攻击方式

原型污染的攻击方式通常利用了 JavaScript 中一些不安全的特性,比如:

  1. 递归合并对象: 很多库(比如 Lodash 的 merge 函数)在合并对象时,如果没有进行严格的检查,就可能被利用来修改原型。
  2. JSON 解析: 如果直接使用 JSON.parse 解析用户输入,并且没有对解析后的对象进行验证,就可能被利用来修改原型。
  3. URL 查询参数: 有些应用会直接将 URL 查询参数解析成对象,如果没有进行过滤,就可能被利用来修改原型。

我们一个一个来看。

3.1 递归合并对象

许多 JavaScript 库提供了深度合并对象的功能。 如果这些函数没有适当地清理输入,攻击者可以通过构造包含 __proto__ 的特殊输入来修改原型。

// 假设我们有一个简单的合并函数
function merge(target, source) {
  for (const key in source) {
    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];
    }
  }
  return target;
}

const obj = {};
const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');

merge(obj, maliciousPayload);

console.log(obj.isAdmin); // 输出:undefined
console.log({}.isAdmin); // 输出:true  原型被污染了!

3.2 JSON 解析

如果应用程序接受 JSON 输入并将其直接解析为对象,攻击者可以注入 __proto__ 属性来修改原型。

const maliciousPayload = '{"__proto__": {"isAdmin": true}}';
const obj = JSON.parse(maliciousPayload);

console.log(obj.isAdmin); // 输出:undefined
console.log({}.isAdmin); // 输出:true  原型被污染了!

3.3 URL 查询参数

当应用程序从 URL 查询参数构建对象时,如果未正确验证,攻击者可以使用 __proto__ 属性注入。

// 模拟从 URL 查询参数创建对象
function createObjectFromParams(params) {
  const obj = {};
  for (const key in params) {
    obj[key] = params[key];
  }
  return obj;
}

const params = { "__proto__.isAdmin": "true" }; // 模拟 URL 查询参数
const obj = createObjectFromParams(params);

console.log(obj.isAdmin); // 输出:undefined
console.log({}.isAdmin); // 输出:true  原型被污染了!

四、 Prototype Pollution 的防御

防御 Prototype Pollution 就像是在你的代码周围建立一道道防线,让攻击者无从下手。

  1. 使用 Object.freeze() 可以冻结对象,防止其属性被修改。
  2. 使用 Object.create(null) 创建一个没有原型的对象,避免继承 Object.prototype
  3. 使用 MapSet 这些数据结构不继承自 Object.prototype,可以避免原型污染。
  4. 验证和清理用户输入: 对用户输入进行严格的验证和清理,防止恶意代码注入。
  5. 使用安全的库: 选择经过安全审计的库,避免使用存在原型污染漏洞的库。
  6. 内容安全策略 (CSP): 限制可以加载的资源,防止恶意脚本执行。
  7. 监控和日志: 监控应用程序的行为,及时发现和处理原型污染攻击。

我们来详细看看每种防御方法,并提供一些示例代码。

4.1 使用 Object.freeze()

Object.freeze() 可以冻结一个对象,使其属性无法被修改、添加或删除。 这是一种简单有效的防御方法,但需要注意的是,Object.freeze() 只能冻结对象自身,而不能冻结对象引用的其他对象。

const obj = { name: 'John', age: 30 };
Object.freeze(obj);

obj.name = 'Jane'; // 尝试修改属性
console.log(obj.name); // 输出:John  修改失败

obj.isAdmin = true; // 尝试添加属性
console.log(obj.isAdmin); // 输出:undefined  添加失败

delete obj.age; // 尝试删除属性
console.log(obj.age); // 输出:30  删除失败

// 冻结原型
Object.freeze(Object.prototype); // 谨慎使用,可能会影响全局行为

4.2 使用 Object.create(null)

Object.create(null) 可以创建一个没有原型的对象。 这种对象不继承自 Object.prototype,因此可以避免原型污染。

const obj = Object.create(null);

obj.__proto__ = { isAdmin: true }; // 尝试修改原型
console.log(obj.isAdmin); // 输出:undefined  修改失败

console.log({}.isAdmin); // 输出:undefined  原型没有被污染

4.3 使用 MapSet

MapSet 是 ES6 引入的新数据结构,它们不继承自 Object.prototype,因此可以避免原型污染。

const map = new Map();
map.set('__proto__', { isAdmin: true });

console.log(map.get('__proto__')); // 输出:{ isAdmin: true }  Map 存储了键值对,但不会污染原型

console.log({}.isAdmin); // 输出:undefined  原型没有被污染

const set = new Set();
set.add('__proto__');

console.log(set.has('__proto__')); // 输出:true  Set 存储了值,但不会污染原型

console.log({}.isAdmin); // 输出:undefined  原型没有被污染

4.4 验证和清理用户输入

这是最重要的一道防线。 对用户输入进行严格的验证和清理,可以防止恶意代码注入。 不要信任任何用户输入!

function sanitizeInput(input) {
  // 1. 检查输入是否包含 __proto__, constructor, prototype 关键字
  if (typeof input === 'string' && (input.includes('__proto__') || input.includes('constructor') || input.includes('prototype'))) {
    return null; // 拒绝包含恶意关键字的输入
  }

  // 2. 如果输入是对象,递归地清理
  if (typeof input === 'object' && input !== null) {
    for (const key in input) {
      if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
        delete input[key]; // 删除恶意属性
      } else {
        input[key] = sanitizeInput(input[key]); // 递归清理
      }
    }
  }

  return input;
}

const maliciousPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
const sanitizedPayload = sanitizeInput(maliciousPayload);

console.log(sanitizedPayload); // 输出:{}  恶意属性被删除了

console.log({}.isAdmin); // 输出:undefined  原型没有被污染

// 使用示例:合并对象时进行清理
function safeMerge(target, source) {
  const sanitizedSource = sanitizeInput(source);
  for (const key in sanitizedSource) {
    if (typeof sanitizedSource[key] === 'object' && sanitizedSource[key] !== null && typeof target[key] === 'object' && target[key] !== null) {
      safeMerge(target[key], sanitizedSource[key]);
    } else {
      target[key] = sanitizedSource[key];
    }
  }
  return target;
}

const obj = {};
const anotherMaliciousPayload = JSON.parse('{"name": "John", "__proto__": {"isAdmin": true}}');
safeMerge(obj, anotherMaliciousPayload);

console.log(obj); // 输出:{ name: 'John' }  恶意属性被删除了

console.log({}.isAdmin); // 输出:undefined  原型没有被污染

4.5 使用安全的库

选择经过安全审计的库,避免使用存在原型污染漏洞的库。 在选择库的时候,多看看有没有相关的安全报告。

如果必须使用可能存在漏洞的库,可以考虑使用沙箱环境来隔离代码,或者使用工具来检测代码中是否存在原型污染漏洞。

4.6 内容安全策略 (CSP)

内容安全策略 (CSP) 是一种浏览器安全机制,可以限制可以加载的资源,防止恶意脚本执行。 通过配置 CSP,可以有效地防御 XSS 攻击,从而降低原型污染的风险。

CSP 通过 HTTP 响应头来配置。 例如,以下 CSP 策略只允许从同一域名加载脚本:

Content-Security-Policy: default-src 'self'

更严格的配置:

Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';

这个策略只允许从同一来源加载脚本、连接、图片和样式,并且禁止加载任何其他资源。

4.7 监控和日志

监控应用程序的行为,及时发现和处理原型污染攻击。 可以记录关键操作,比如对象属性的修改,以及异常事件,比如未知的属性访问。

// 监控对象属性的修改
function monitorProperty(obj, property) {
  let value = obj[property];
  Object.defineProperty(obj, property, {
    get: function() {
      return value;
    },
    set: function(newValue) {
      console.warn(`Property ${property} of object ${obj} is being modified from ${value} to ${newValue}`);
      // 记录日志
      logEvent(`Property ${property} modified`, { object: obj, oldValue: value, newValue: newValue });
      value = newValue;
    }
  });
}

// 示例:监控 Object.prototype.isAdmin
monitorProperty(Object.prototype, 'isAdmin');

Object.prototype.isAdmin = true; // 触发警告和日志

五、Prototype Pollution 的检测工具

有一些工具可以帮助你检测代码中是否存在原型污染漏洞。 例如:

  • eslint-plugin-prototype-pollution 一个 ESLint 插件,可以检测代码中是否存在原型污染的风险。
  • safe-eval 一个安全的 JavaScript 代码执行环境,可以防止原型污染。

六、总结

Prototype Pollution 是一种隐蔽而危险的漏洞,需要引起足够的重视。 通过采取合适的防御措施,可以有效地降低原型污染的风险,保护你的应用程序安全。

记住以下几点:

  • 不要信任任何用户输入。
  • 使用安全的库。
  • 验证和清理用户输入。
  • 监控应用程序的行为。
  • 及时更新你的库和框架。

希望今天的讲座能帮助你更好地理解和防御 Prototype Pollution。 谢谢大家!

一个表格总结防御方法:

防御方法 描述 优点 缺点 适用场景
Object.freeze() 冻结对象,使其属性无法被修改、添加或删除。 简单易用,效果明显。 只能冻结对象自身,无法冻结对象引用的其他对象。 冻结 Object.prototype 可能会影响全局行为,需要谨慎使用。 适用于需要保护对象不被修改的场景。
Object.create(null) 创建一个没有原型的对象,避免继承 Object.prototype 可以完全避免原型污染。 创建的对象没有继承任何原型方法,使用时需要注意。 适用于不需要原型继承的场景,例如存储数据。
MapSet 使用 MapSet 数据结构,它们不继承自 Object.prototype 可以避免原型污染。 不适用于需要使用对象属性的场景。 适用于存储键值对或唯一值的场景。
验证和清理用户输入 对用户输入进行严格的验证和清理,防止恶意代码注入。 可以有效地防止原型污染攻击。 需要编写大量的验证和清理代码,容易出错。 适用于所有需要处理用户输入的场景。
使用安全的库 选择经过安全审计的库,避免使用存在原型污染漏洞的库。 可以避免使用存在漏洞的库,降低风险。 需要花费时间和精力来选择和评估库。 适用于选择第三方库的场景。
内容安全策略 (CSP) 限制可以加载的资源,防止恶意脚本执行。 可以有效地防御 XSS 攻击,从而降低原型污染的风险。 配置比较复杂,需要仔细考虑。 适用于需要防御 XSS 攻击的场景。
监控和日志 监控应用程序的行为,及时发现和处理原型污染攻击。 可以及时发现和处理原型污染攻击。 需要编写监控和日志代码,并且需要定期检查日志。 适用于需要监控应用程序行为的场景。

希望这个表格能帮助你更好地理解各种防御方法的优缺点和适用场景。 记住,没有银弹,需要根据实际情况选择合适的防御方法,并结合使用多种方法,才能有效地保护你的应用程序安全。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注