摘要: 详解原型污染。

Fundebug经授权转载,版权归原作者所有。

可能有信息敏感的同学已经了解到:Lodash 库爆出严重安全漏洞,波及 400万+ 项目。这个漏洞使得 lodash “连夜”发版以解决潜在问题,并强烈建议开发者升级版本。

我们在忙着“看热闹”或者“”升级版本”的同时,静下心来想:真的有理解这个漏洞产生的原因,明白漏洞修复背后的原理了吗?

这篇短文将从原理层面分析这一事件,相信“小白”读者会有所收获。

漏洞原因

其实漏洞很简单,举一个例子:lodash 中 defaultsDeep 方法,

_.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } })

输出:

{ 'a': { 'b': 2, 'c': 3 } }

如上例,该方法:

分配来源对象(该方法的第二个参数)的可枚举属性到目标对象(该方法的第一个参数)所有解析为 undefined 的属性上

这样的操作存在的隐患:

const payload = '{"constructor": {"prototype": {"toString": true}}}'

_.defaultsDeep({}, JSON.parse(payload))

如此一来,就触发了原型污染。原型污染是指:

攻击者通过某种手段修改 JavaScript 对象的原型(prototype)

对应上例,Object.prototype.toString 就会非常不安全了。

详解原型污染

理解原型污染,需要读者理解 JavaScript 当中的原型、原型链的知识。我们先来看一个例子:

// person 是一个简单的 JavaScript 对象
let person = {name: 'lucas'} // 输出 lucas
console.log(person.name) // 修改 person 的原型
person.__proto__.name = 'messi' // 由于原型链顺序查找的原因,person.name 仍然是 lucas
console.log(person.name) // 再创建一个空的 person2 对象
let person2 = {} // 查看 person2.name,输出 messi
console.log(person2.name)

把危害扩大化:

let person = {name: 'lucas'}

console.log(person.name)

person.__proto__.toString = () => {alert('evil')}

console.log(person.name)

let person2 = {}

console.log(person2.toString())

这段代码执行将会 alert 出 evil 文字。同时 Object.prototype.toString 这个方法会在隐式转换以及类型判断中经常被用到:

Object.prototype.toString 方法返回一个表示该对象的字符串

每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 [object type],其中 type 是对象的类型。

如果 Object 原型上的 toString 被污染,后果可想而知。以此为例,可见 lodash 这次漏洞算是比较严重了。

再谈原型污染(NodeJS 漏洞案例)

由上分析,我们知道原型污染并不是什么新鲜的漏洞,它“随时可见”,“随处可见”。在 Nullcon HackIM 比赛中就有一个类似的 hack 题目:

'use strict';

const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path'); const isObject = obj => obj && obj.constructor && obj.constructor === Object; function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
} function clone(a) {
return merge({}, a);
} // Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {}; // App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser()); app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if (copybody.name) {
res.cookie('name', copybody.name).json({
"done": "cookie set"
});
} else {
res.json({
"error": "cookie not set"
})
}
});
app.get('/getFlag', (req, res) => {
var аdmin = JSON.parse(JSON.stringify(req.cookies))
if (admin.аdmin == 1) {
res.send("hackim19{}");
} else {
res.send("You are not authorized");
}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

这段代码的漏洞就在于 merge 函数上,我们可以这样攻击:

curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://0.0.0.0:4000/signup'; 

curl -vv 'http://0.0.0.0:4000/getFlag'

首先请求 /signup 接口,在 NodeJS 服务中,我们调用了有漏洞的 merge 方法,并通过 __proto__Object.prototype(因为 {}.__proto__ === Object.prototype) 添加上一个新的属性 admin,且值为 1。

再次请求 getFlag 接口,条件语句 admin.аdmin == 1true,服务被攻击。

攻击案例出自:Prototype pollution attacks in NodeJS applications

这样的漏洞在 jQuery $.extend 中也经常见到:

对于 jQuery:如果担心安全问题,建议升级至最新版本 jQuery 3.4.0,如果还在使用 jQuery 的 1.x 和 2.x 版本,那么你的应用程序和网站仍有可能遭受攻击。

防范原型污染

了解了漏洞潜在问题以及攻击手段,那么如何防范呢?

在 lodash “连夜”发版的修复中:

我们可以清晰的看到,在遍历 merge 时,当遇见 constructor 以及 __proto__ 敏感属性,则退出程序。

那么作为业务开发者,我们需要注意些什么,防止攻击出现呢?总结一下有:

  • 冻结 Object.prototype,使原型不能扩充属性

我们可以采用 Object.freeze 达到目的:

Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

看代码:

Object.freeze(Object.prototype);

Object.prototype.toString = 'evil'

consoel.log(Object.prototype.toString)
ƒ toString() { [native code] }

对比:

Object.prototype.toString = 'evil'

console.log(Object.prototype.toString)
"evil"
  • 建立 JSON schema

    在解析用户输入内容是,通过 JSON schema 过滤敏感键名。
  • 规避不安全的递归性合并

    这一点类似 lodash 修复手段,完善了合并操作的安全性,对敏感键名跳过处理
  • 使用无原型对象

    在创建对象时,不采用字面量方式,而是使用 Object.create(null)

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

Object.create(null) 的返回值不会链接到 Object.prototype

let foo = Object.create(null)
console.log(foo.__proto__)
// undefined

这样一来,无论如何扩充对象,都不会干扰到原型了。

  • 采用新的 Map 数据类型,代替 Object 类型

Map 对象保存键/值对,是键/值对的集合。任何值(对象或者原始值)都可以作为一个键或一个值。使用 Map 数据结构,不会存在 Object 原型污染状况。

这里总结一下 Map 和 Object 不同点::

  • Object 的键只支持 String 或者 Symbols 两种类型,Map 的键可以是任意值,包括函数、对象、基本类型
  • Map 中的键值是有序的,而 Object 中的键则不是
  • 具体 API 上的差异:比如,通过 size 属性直接获取一个 Map 的键值对个数,而 Object 的键值无法获取;再比如迭代一个 Map 和 Object 差异也比较明显
  • Map 在频繁增删键值对的场景下会有些性能优势

补充:V8,chromium 的小机灵

同样存在风险的是我们常用的 JSON.parse 方法,但是如果你运行:

JSON.parse('{ "a":1, "__proto__": { "b": 2 }}')

你会发现返回的结果如图:

复写 Object.prototype 失败了,__proto__ 属性还是我们熟悉的那个有安全感的 __proto__ 。这是因为:

V8 ignores keys named proto in JSON.parse

这个相关讨论 Doug Crockford,Brendan Eich,反正 chromium 和 JS 发明人讨论过很多次。相关 issue 和 PR:

相关 ES 语言设计的讨论:ES 语言设计的讨论:proto-and-json

在上面链接中,你能发现 JavaScript 发明人等一众大佬哦~

总之你可以记住,V8 默认使用 JSON.parse 时候会忽略 __proto__,原因当然是之前分析的安全性了。

总结

通过分析 lodash 的漏洞,以及解决方案,我们了解了原型污染的方方面面。涉及到的知识点包括但不限于:

  • Object 原型
  • 原型、原型链
  • NodeJS 相关问题
  • Object.create 方法
  • Object.freeze 方法
  • Map 数据结构
  • 深拷贝
  • 以及其他问题

这么来看,全是基础知识。也正是基础,构成了前端知识体系的方方面面。

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!

Lodash 严重安全漏洞背后 你不得不知道的 JavaScript 知识的更多相关文章

  1. 震惊!90%的程序员不知道的Java知识!

    震惊!90%的程序员不知道的Java知识! 初学Java的时候都会接触的代码 public static void main(String[] args){ ... } 当时就像背公式一样把这行代码给 ...

  2. js值----你所不知道的JavaScript系列(6)

    1.数组 在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串.数字.对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的) .----<你所不知道的JavaS ...

  3. js类型----你所不知道的JavaScript系列(5)

    ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型等. 1.内置类型 JavaScript 有七种内置类型: • 空值(null) • 未定义( ...

  4. 闭包----你所不知道的JavaScript系列(4)

    一.闭包是什么? · 闭包就是可以使得函数外部的对象能够获取函数内部的信息. · 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. · 闭包就 ...

  5. let和const----你所不知道的JavaScript系列(2)

    let 众所周知,在ES6之前,声明变量的关键字就只有var.var 声明变量要么是全局的,要么是函数级的,而无法是块级的. var a=1; console.log(a); console.log( ...

  6. LHS 和 RHS----你所不知道的JavaScript系列(1)

      变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值.----<你所不知道的Ja ...

  7. 【转载】14个你可能不知道的 JavaScript 调试技巧

    了解你的工具可以极大的帮助你完成任务.尽管 JavaScript 的调试非常麻烦,但在掌握了技巧 (tricks) 的情况下,你依然可以用尽量少的的时间解决这些错误 (errors) 和问题 (bug ...

  8. 14 个你可能不知道的 JavaScript 调试技巧

    了解你的工具可以极大的帮助你完成任务.尽管 JavaScript 的调试非常麻烦,但在掌握了技巧 (tricks) 的情况下,你依然可以用尽量少的的时间解决这些错误 (errors) 和问题 (bug ...

  9. 你所不知道的JavaScript数组

    相信每一个 javascript 学习者,都会去了解 JS 的各种基本数据类型,数组就是数据的组合,这是一个很基本也十分简单的概念,他的内容没多少,学好它也不是件难事情.但是本文着重要介绍的并不是我们 ...

随机推荐

  1. 7.JavaCC官方入门指南-例2

    例2:整数加法运算--改良版(增强语法分析器) 1.修改   上一个例子中,JavaCC为BNF生产式所生成的方法,比如Start(),这些方法默认只简单的检查输入是否匹配BNF生产式指定的规范.但是 ...

  2. man -k, man -f : nothing appropriate ; 更新 whatis 数据库

    man 有两个选项: -f, –whatis Equivalent to whatis. Display a ) for details. -k, –apropos Equivalent to apr ...

  3. python简单面试题

    在这个即将进入金9银10的跳槽季节的时候,肯定需要一波面试题了,安静总结了一些经常遇到的python面试题,让我们一起撸起来. python面试题 1.求出1-100之间的和 # coidng:utf ...

  4. python 实现 AES CBC模式加解密

    AES加密方式有五种:ECB, CBC, CTR, CFB, OFB 从安全性角度推荐CBC加密方法,本文介绍了CBC,ECB两种加密方法的python实现 python 在 Windows下使用AE ...

  5. logistic 回归(线性和非线性)

    一:线性logistic 回归 代码如下: import numpy as np import pandas as pd import matplotlib.pyplot as plt import ...

  6. Java基本数据类型转换二

    public class TestConvert2 { /** * @param args */ public static void main(String[] args) { // TODO Au ...

  7. day71_10_16多表断关联

    ---恢复内容开始--- 本次环境: 配置settings INSTALLED_APPS = [ # ... 'rest_framework', ] DATABASES = { 'default': ...

  8. 剑指Offer-5.用两个栈实现队列(C++/Java)

    题目: 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 分析: 栈的特点是先进后出,队列的特点则是先进先出. 题目要求我们用两个栈来实现一个队列,栈和队列都有入栈 ...

  9. flask-windows部署

    由于supervisor不支持windows,但要防止程序异常中断,所以需要采取措施 通过pywin32,使得flask以服务的方式运行 创建pythonservice.py import win32 ...

  10. ubuntu中安装rabbitmq服务并成功启动

    在我们使用rabbitmq时,首先要对其进行安装,而后才能对其进行使用 安装 Erlang 由于 RabbitMQ 是采用 Erlang 编写的,所以需要安装 Erlang 语言库.就像 java 需 ...