关于Lodash安全漏洞背后的 JavaScript,你需要了解什么
一些信息敏感的同学可能已经了解到,Lodash的图书馆专业 refoam-security,影响了超过 400 万个项目。该漏洞导致lodash发布了“连夜”版本来修复潜在问题,并强烈鼓励开发者更新该版本。
当我们忙着“好看”或者“优化版本”的时候,请保持冷静思考:你真的了解这个漏洞的原因和背后的原理吗?修复这个漏洞吗?
这个简短的文章将从根本上分析这个案例,相信“小白”读者会有所收获。
漏洞产生的原因
其实漏洞很简单。举个例子:lodash中的defaultsDeep方法,
_.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } })
输出:
{ 'a': { 'b': 2, 'c': 3 } }
根据上面的例子,这个方法:
返回任意属性的源对象(该方法的第二个参数)的可枚举属性。目标对象(方法的第一个参数)决定不确定
这种行为的隐患:
const payload = '{"constructor": {"prototype": {"toString": true}}}'
_.defaultsDeep({}, JSON.parse(payload))
造成原型污染。原型污染是指:
攻击者以某种方式修改了 JavaScript 对象的原型
与上面的例子相比,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())
执行此代码将警告恶意文本。同时,Object.prototype.toString这个方法常用于转换和类型判断。用法:
Object.prototype.toString 方法返回表示对象的字符串
每个对象都有一个方法 toString(),当对象表示为文本值时,或者当它被调用时其本身被称为预期字符串。默认情况下,所有 Object 对象都会继承 toString() 方法。如果未在自定义对象上重写此方法,则 toString() 返回 [对象类型],这是对象类型的类型。
如果对象原型上的 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}`);
此代码的漏洞在于连接。我们可以这样攻击它:
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'首先请求NodeJS服务中的 ♸ 攻击案例来自:NodeJS应用原型污染攻击 此类漏洞在jQuery中也很常见 对于 jQuery:如果您担心安全问题,请更新到最新的 jQuery 3.4.0。如果您仍在使用 jQuery 1.x 和 2.x,您的应用程序和网站可能容易受到攻击。 既然我们了解了漏洞和攻击方法的潜在问题,那么我们该如何预防呢? 关于lodash版“一夜”的构造: 我们清楚地看到,当我们交叉合并时,当我们遇到 那么作为业务开发者,我们应该如何防范攻击呢?简而言之: 我们可以使用 查看代码: 比较: Object.create() 方法创建一个新对象,使用现有对象分配新创建对象的 。 __proto__ 这样,无论对象的扩展名是什么,不干扰原型。 。 Map存储键/值对和键/货币对的集合。任何值(对象或基元)都可以用作键或值。通过使用Map数据结构,不存在Object原型污染。 总结一下 Map 和 Object 的区别: ,你会看到返回的结果如图: 重复写入 V8 忽略 JSON.parse 中名为 proto 的键 此讨论与 Doug Crockford、Brendan Eich 相关,但是,Chromium 和 JS 的创建者已多次解决此问题。相关问题和PR: 相关ES语言设计讨论:ES语言设计讨论:proto-and-json 在上面的链接中,你可以找到JavaScript的开发者等等对于成年人~ 简而言之,请注意,V8 默认情况下使用 通过分析lodash的弱点和解决办法,了解原型污染的各个方面。包含的知识点包括但不限于: 这样看来,都是基础知识了。它也是构成正面信息系统各方面的基础。/signup接口,称为易受攻击的merge,例如![]()
Object.prototype(因为{}.__pro to__ === Object.prototype)将新属性admin添加到请求接口的值admin 再次getFlag,条件语句admin.аadmin == 1为true服务被攻击。 $.extend PR:避免原型污染
![]()
构造函数和__程序,exsensitive,是 __pro 。 Object.prototype,这样原型就无法扩展属性Object.Object来实现cool()方法,有的东西会冻结。冻结的东西无法改变;当对象被冻结时,无法向该对象添加新属性,无法删除现有属性,并且无法更改该对象现有属性的枚举、可配置性和注册。 property,并且现有的属性值无法更改。此外,对象的原型在冻结后就无法更改。 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过滤敏感键名。 ?不要使用文字,而是使用 Object.create(null): Object.create(null)的返回值不会与Object.prototype关联:let foo = Object.create(null)
console.log(foo.__proto__)
// undefined
附录:V8,chromium的能力
JSON .parse 常用方法,但如果运行:JSON.parse('{ "a":1, "__proto__": { "b": 2 }}')
![]()
‷‷失败 __原型__ 财产仍然是我们的 熟悉的感觉安全 __proto__ 。这是因为: JSON.parse 时会忽略 __proto__。 .当然,原因是为了保护之前的调查。 总结
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网
