Lodash 严重安全漏洞背后 你不得不知道的 JavaScript 知识
摘要: 详解原型污染。
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 == 1
为 true
,服务被攻击。
攻击案例出自: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 知识的更多相关文章
- 震惊!90%的程序员不知道的Java知识!
震惊!90%的程序员不知道的Java知识! 初学Java的时候都会接触的代码 public static void main(String[] args){ ... } 当时就像背公式一样把这行代码给 ...
- js值----你所不知道的JavaScript系列(6)
1.数组 在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串.数字.对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的) .----<你所不知道的JavaS ...
- js类型----你所不知道的JavaScript系列(5)
ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型等. 1.内置类型 JavaScript 有七种内置类型: • 空值(null) • 未定义( ...
- 闭包----你所不知道的JavaScript系列(4)
一.闭包是什么? · 闭包就是可以使得函数外部的对象能够获取函数内部的信息. · 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. · 闭包就 ...
- let和const----你所不知道的JavaScript系列(2)
let 众所周知,在ES6之前,声明变量的关键字就只有var.var 声明变量要么是全局的,要么是函数级的,而无法是块级的. var a=1; console.log(a); console.log( ...
- LHS 和 RHS----你所不知道的JavaScript系列(1)
变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值.----<你所不知道的Ja ...
- 【转载】14个你可能不知道的 JavaScript 调试技巧
了解你的工具可以极大的帮助你完成任务.尽管 JavaScript 的调试非常麻烦,但在掌握了技巧 (tricks) 的情况下,你依然可以用尽量少的的时间解决这些错误 (errors) 和问题 (bug ...
- 14 个你可能不知道的 JavaScript 调试技巧
了解你的工具可以极大的帮助你完成任务.尽管 JavaScript 的调试非常麻烦,但在掌握了技巧 (tricks) 的情况下,你依然可以用尽量少的的时间解决这些错误 (errors) 和问题 (bug ...
- 你所不知道的JavaScript数组
相信每一个 javascript 学习者,都会去了解 JS 的各种基本数据类型,数组就是数据的组合,这是一个很基本也十分简单的概念,他的内容没多少,学好它也不是件难事情.但是本文着重要介绍的并不是我们 ...
随机推荐
- 7.JavaCC官方入门指南-例2
例2:整数加法运算--改良版(增强语法分析器) 1.修改 上一个例子中,JavaCC为BNF生产式所生成的方法,比如Start(),这些方法默认只简单的检查输入是否匹配BNF生产式指定的规范.但是 ...
- man -k, man -f : nothing appropriate ; 更新 whatis 数据库
man 有两个选项: -f, –whatis Equivalent to whatis. Display a ) for details. -k, –apropos Equivalent to apr ...
- python简单面试题
在这个即将进入金9银10的跳槽季节的时候,肯定需要一波面试题了,安静总结了一些经常遇到的python面试题,让我们一起撸起来. python面试题 1.求出1-100之间的和 # coidng:utf ...
- python 实现 AES CBC模式加解密
AES加密方式有五种:ECB, CBC, CTR, CFB, OFB 从安全性角度推荐CBC加密方法,本文介绍了CBC,ECB两种加密方法的python实现 python 在 Windows下使用AE ...
- logistic 回归(线性和非线性)
一:线性logistic 回归 代码如下: import numpy as np import pandas as pd import matplotlib.pyplot as plt import ...
- Java基本数据类型转换二
public class TestConvert2 { /** * @param args */ public static void main(String[] args) { // TODO Au ...
- day71_10_16多表断关联
---恢复内容开始--- 本次环境: 配置settings INSTALLED_APPS = [ # ... 'rest_framework', ] DATABASES = { 'default': ...
- 剑指Offer-5.用两个栈实现队列(C++/Java)
题目: 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 分析: 栈的特点是先进后出,队列的特点则是先进先出. 题目要求我们用两个栈来实现一个队列,栈和队列都有入栈 ...
- flask-windows部署
由于supervisor不支持windows,但要防止程序异常中断,所以需要采取措施 通过pywin32,使得flask以服务的方式运行 创建pythonservice.py import win32 ...
- ubuntu中安装rabbitmq服务并成功启动
在我们使用rabbitmq时,首先要对其进行安装,而后才能对其进行使用 安装 Erlang 由于 RabbitMQ 是采用 Erlang 编写的,所以需要安装 Erlang 语言库.就像 java 需 ...