从lodash原型污染安全漏洞深入理解JavaScript原型机制

7月9日,著名的lodash被爆出存在严重的安全漏洞。(原文链接)
具体是defaultsDeep这个方法,使用不当会造成原型污染,并带来不可预知的恶劣影响。该方法用来递归合并对象,官方文档是这么介绍的:

从lodash原型污染安全漏洞深入理解JavaScript原型机制_第1张图片
defaultsDeep

可以看到,这个方法用来将一个对象里面另一个对象中不存在的键深拷贝到后者当中。下面的示例展示了该方法是如何污染原型的:

const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"a0": true}}}'

function check() {
    mergeFn({}, JSON.parse(payload));
    if (({})[`a0`] === true) {
        console.log(`Vulnerable to Prototype Pollution via ${payload}`);
    }
  }

check();

payload是需要合并进目标对象的一个JSON字符串,解析为对象后,通过defaultsDeep方法,可以将其内部的键复制给一个空对象,此时空对象的constructor的原型成功写入了一个键值对“a0: true”。此后所有的对象的原型上都会多出这个键值对。

要想真正理解这一段代码,必须先理解JavaScript对象的原型链和继承。如下图所示,任何一个实例对象都有一个私有属性__proto__指向它的原型对象,原型对象包含一个构造函数(constructor),使用这个构造函数可以实例化这个原型对象,生成实例对象。

从lodash原型污染安全漏洞深入理解JavaScript原型机制_第2张图片
原型关系图

而每个原型对象又有自己的原型对象,层层向上,到达原型链的顶端Object,Object的原型对象指向null,既Object没有原型对象。几乎所有对象都是Object的实例。

ECMAScript标准规定someObject.[[Prototype]]指向someObject的原型,而ES6实现了通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器访问[[Prototype]],等同于__proto__。__proto__虽然不是标准,但很多浏览器已经实现了。

在chrome控制台打印一个空对象(一个对象实例)的原型,可以看到其原型即为Object对象:


从lodash原型污染安全漏洞深入理解JavaScript原型机制_第3张图片
一个对象的原型

打印这个空对象的原型的原型(即Object的原型),得到null:


原型链的顶端Object的原型是null

了解了原型链,现在来了解对象的继承机制。先来看一个有趣的问题:

function A () {};
function B () {};
B.prototype = new A();
let a = new A();
let b = new B();
log(a.constructor); // A
log(b.constructor); // A

在上面这段代码中,有两个构建函数A和B,构造函数B的原型指向构造函数A的实例,此时B继承了A。然后分别实例化A和B,打印实例对象a和b的构造函数,却发现纷纷指向了A。问题来了,为什么实例对象b的构造函数不指向B呢?

我个人的理解是:B原本有自己的原型对象B.protptype,当B.protptype指向了A的一个实例时,B已经跟B原先的原型对象没关系了,此时B的原型对象变为了A的一个实例,B继承了A的实例,b作为B的实例,继承原型链上所有属性。b作为实例本身不含有constructor属性,而构造函数B“背叛了”它原来的主子(原型对象),认A的一个实例作父(原型),而A的实例同样本身不具有constructor属性,但是A的原型具有一个constructor属性,指向构造函数A。b.constructor顺着原型链一直向上,便会找到constructor,所以最终的结果b.constructor指向了A。

再回到lodash的例子中,空对象的constructor的prototype,即Object中写入了一个a0属性,实现了原型篡改,那么后续所有对象的原型都会带上这个a0。

const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"a0": true}}}'

function check() {
    mergeFn({}, JSON.parse(payload));
    if (({})[`a0`] === true) {
        console.log(`Vulnerable to Prototype Pollution via ${payload}`);
    }
  }

check();

注:7月9日lodash发布了4.17.12版本,修复了这个漏洞。

你可能感兴趣的:(从lodash原型污染安全漏洞深入理解JavaScript原型机制)