
摘要: 详解原型污染。
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'}// 输出 lucasconsole.log(person.name)// 修改 person 的原型person.__proto__.name = 'messi'// 因为原型链顺序查找的起因,person.name 依然是 lucasconsole.log(person.name)// 再创立一个空的 person2 对象let person2 = {}// 查看 person2.name,输出 messiconsole.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 这次漏洞算是比较严重了。
由上分析,我们知道原型污染并不是什么新鲜的漏洞,它“随时可见”,“随处可见”。在 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);} // Constantsconst PORT = 8080;const HOST = '0.0.0.0';const admin = {}; // Appconst 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"Object.create(null):Object.create()方法创立一个新对象,使用现有的对象来提供新创立的对象的
__proto__
Object.create(null) 的返回值不会链接到 Object.prototype:
let foo = Object.create(null)console.log(foo.__proto__)// undefined这样一来,无论如何扩充对象,都不会干扰到原型了。
Map 对象保存键/值对,是键/值对的集合。任何值(对象或者者原始值)都可以作为一个键或者一个值。使用 Map 数据结构,不会存在 Object 原型污染状况。
这里总结一下 Map 和 Object 不同点::
同样存在风险的是我们常用的 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 的漏洞,以及处理方案,我们理解了原型污染的方方面面。涉及到的知识点包括但不限于:
这么来看,全是基础知识。也正是基础,构成了前台知识体系的方方面面。
Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计解决了10亿+错误事件,付费用户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!