Lodash 嚴(yán)重安全漏洞背后 你不得不知道的 JavaScript 知識(shí)
作者:Lucas HC
原文:https://zhuanlan.zhihu.com/p/73186974
可能有信息敏感的同學(xué)已經(jīng)了解到:Lodash 庫爆出嚴(yán)重安全漏洞,波及 400萬+ 項(xiàng)目。這個(gè)漏洞使得 lodash “連夜”發(fā)版以解決潛在問題,并強(qiáng)烈建議開發(fā)者升級(jí)版本。
我們?cè)诿χ翱礋狒[”或者“”升級(jí)版本”的同時(shí),靜下心來想:真的有理解這個(gè)漏洞產(chǎn)生的原因,明白漏洞修復(fù)背后的原理了嗎?
這篇短文將從原理層面分析這一事件,相信“小白”讀者會(huì)有所收獲。
漏洞原因
其實(shí)漏洞很簡(jiǎn)單,舉一個(gè)例子:lodash 中 defaultsDeep 方法,
_.defaultsDeep({?'a':?{?'b':?2?}?},?{?'a':?{?'b':?1,?'c':?3?}?})
輸出:
{?'a':?{?'b':?2,?'c':?3?}?}
如上例,該方法:
分配來源對(duì)象(該方法的第二個(gè)參數(shù))的可枚舉屬性到目標(biāo)對(duì)象(該方法的第一個(gè)參數(shù))所有解析為 undefined 的屬性上
這樣的操作存在的隱患:
const?payload?=?'{"constructor":?{"prototype":?{"toString":?true}}}'
_.defaultsDeep({},?JSON.parse(payload))
如此一來,就觸發(fā)了原型污染。原型污染是指:
攻擊者通過某種手段修改 JavaScript 對(duì)象的原型(prototype)
對(duì)應(yīng)上例,Object.prototype.toString 就會(huì)非常不安全了。
詳解原型污染
理解原型污染,需要讀者理解 JavaScript 當(dāng)中的原型、原型鏈的知識(shí)。我們先來看一個(gè)例子:
//?person?是一個(gè)簡(jiǎn)單的?JavaScript?對(duì)象
let?person?=?{name:?'lucas'}
//?輸出?lucas
console.log(person.name)
//?修改?person?的原型
person.__proto__.name?=?'messi'
//?由于原型鏈順序查找的原因,person.name?仍然是?lucas
console.log(person.name)
//?再創(chuàng)建一個(gè)空的?person2?對(duì)象
let?person2?=?{}
//?查看?person2.name,輸出?messi
console.log(person2.name)
把危害擴(kuò)大化:
let?person?=?{name:?'lucas'}
console.log(person.name)
person.__proto__.toString?=?()?=>?{alert('evil')}
console.log(person.name)
let?person2?=?{}
console.log(person2.toString())
這段代碼執(zhí)行將會(huì) alert 出 evil 文字。同時(shí) Object.prototype.toString 這個(gè)方法會(huì)在隱式轉(zhuǎn)換以及類型判斷中經(jīng)常被用到:
Object.prototype.toString 方法返回一個(gè)表示該對(duì)象的字符串
每個(gè)對(duì)象都有一個(gè) toString() 方法,當(dāng)該對(duì)象被表示為一個(gè)文本值時(shí),或者一個(gè)對(duì)象以預(yù)期的字符串方式引用時(shí)自動(dòng)調(diào)用。默認(rèn)情況下,toString() 方法被每個(gè) Object 對(duì)象繼承。如果此方法在自定義對(duì)象中未被覆蓋,toString() 返回 [object type],其中 type 是對(duì)象的類型。
如果 Object 原型上的 toString 被污染,后果可想而知。以此為例,可見 lodash 這次漏洞算是比較嚴(yán)重了。
再談原型污染(NodeJS 漏洞案例)
由上分析,我們知道原型污染并不是什么新鮮的漏洞,它“隨時(shí)可見”,“隨處可見”。在 Nullcon HackIM 比賽中就有一個(gè)類似的 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 函數(shù)上,我們可以這樣攻擊:
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'
首先請(qǐng)求 /signup 接口,在 NodeJS 服務(wù)中,我們調(diào)用了有漏洞的 merge 方法,并通過 __proto__ 為 Object.prototype(因?yàn)?{}.__proto__ === Object.prototype) 添加上一個(gè)新的屬性 admin,且值為 1。
再次請(qǐng)求 getFlag 接口,條件語句 admin.аdmin == 1 為 true,服務(wù)被攻擊。
攻擊案例出自:Prototype pollution attacks in NodeJS applications
這樣的漏洞在 jQuery $.extend 中也經(jīng)常見到:
jQuery 修復(fù)原型污染 PR jQuery prototype pollution vulnerability
對(duì)于 jQuery:如果擔(dān)心安全問題,建議升級(jí)至最新版本 jQuery 3.4.0,如果還在使用 jQuery 的 1.x 和 2.x 版本,那么你的應(yīng)用程序和網(wǎng)站仍有可能遭受攻擊。
防范原型污染
了解了漏洞潛在問題以及攻擊手段,那么如何防范呢?
在 lodash “連夜”發(fā)版的修復(fù)中:

我們可以清晰的看到,在遍歷 merge 時(shí),當(dāng)遇見 constructor 以及 __proto__ 敏感屬性,則退出程序。
那么作為業(yè)務(wù)開發(fā)者,我們需要注意些什么,防止攻擊出現(xiàn)呢?總結(jié)一下有:
凍結(jié) Object.prototype,使原型不能擴(kuò)充屬性
我們可以采用 Object.freeze 達(dá)到目的:
Object.freeze() 方法可以凍結(jié)一個(gè)對(duì)象。一個(gè)被凍結(jié)的對(duì)象再也不能被修改;凍結(jié)了一個(gè)對(duì)象則不能向這個(gè)對(duì)象添加新的屬性,不能刪除已有屬性,不能修改該對(duì)象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結(jié)一個(gè)對(duì)象后該對(duì)象的原型也不能被修改。freeze() 返回和傳入的參數(shù)相同的對(duì)象。
看代碼:
Object.freeze(Object.prototype);
Object.prototype.toString?=?'evil'
consoel.log(Object.prototype.toString)
??toString()?{?[native?code]?}
對(duì)比:
Object.prototype.toString?=?'evil'
console.log(Object.prototype.toString)
"evil"
建立 JSON schema
在解析用戶輸入內(nèi)容是,通過 JSON schema 過濾敏感鍵名。規(guī)避不安全的遞歸性合并
這一點(diǎn)類似 lodash 修復(fù)手段,完善了合并操作的安全性,對(duì)敏感鍵名跳過處理使用無原型對(duì)象
在創(chuàng)建對(duì)象時(shí),不采用字面量方式,而是使用Object.create(null):
Object.create()方法創(chuàng)建一個(gè)新對(duì)象,使用現(xiàn)有的對(duì)象來提供新創(chuàng)建的對(duì)象的
__proto__
Object.create(null) 的返回值不會(huì)鏈接到 Object.prototype:
let?foo?=?Object.create(null)
console.log(foo.__proto__)
//?undefined
這樣一來,無論如何擴(kuò)充對(duì)象,都不會(huì)干擾到原型了。
采用新的 Map 數(shù)據(jù)類型,代替 Object 類型
Map 對(duì)象保存鍵/值對(duì),是鍵/值對(duì)的集合。任何值(對(duì)象或者原始值)都可以作為一個(gè)鍵或一個(gè)值。使用 Map 數(shù)據(jù)結(jié)構(gòu),不會(huì)存在 Object 原型污染狀況。
這里總結(jié)一下 Map 和 Object 不同點(diǎn)::
Object 的鍵只支持 String 或者 Symbols 兩種類型,Map 的鍵可以是任意值,包括函數(shù)、對(duì)象、基本類型 Map 中的鍵值是有序的,而 Object 中的鍵則不是 具體 API 上的差異:比如,通過 size 屬性直接獲取一個(gè) Map 的鍵值對(duì)個(gè)數(shù),而 Object 的鍵值無法獲取;再比如迭代一個(gè) Map 和 Object 差異也比較明顯 Map 在頻繁增刪鍵值對(duì)的場(chǎng)景下會(huì)有些性能優(yōu)勢(shì)
補(bǔ)充:V8,chromium 的小機(jī)靈
同樣存在風(fēng)險(xiǎn)的是我們常用的 JSON.parse 方法,但是如果你運(yùn)行:
JSON.parse('{?"a":1,?"__proto__":?{?"b":?2?}}')
你會(huì)發(fā)現(xiàn)返回的結(jié)果如圖:

復(fù)寫 Object.prototype 失敗了,__proto__ 屬性還是我們熟悉的那個(gè)有安全感的 __proto__ 。這是因?yàn)椋?/p>
V8 ignores keys named proto in JSON.parse
這個(gè)相關(guān)討論 Doug Crockford,Brendan Eich,反正 chromium 和 JS 發(fā)明人討論過很多次。相關(guān) issue 和 PR:
chromium 討論 chromium 討論
相關(guān) ES 語言設(shè)計(jì)的討論:ES 語言設(shè)計(jì)的討論:proto-and-json
在上面鏈接中,你能發(fā)現(xiàn) JavaScript 發(fā)明人等一眾大佬哦~
總之你可以記住,V8 默認(rèn)使用 JSON.parse 時(shí)候會(huì)忽略 __proto__,原因當(dāng)然是之前分析的安全性了。
總結(jié)
通過分析 lodash 的漏洞,以及解決方案,我們了解了原型污染的方方面面。涉及到的知識(shí)點(diǎn)包括但不限于:
Object 原型 原型、原型鏈 NodeJS 相關(guān)問題 Object.create 方法 Object.freeze 方法 Map 數(shù)據(jù)結(jié)構(gòu) 深拷貝 以及其他問題
這么來看,全是基礎(chǔ)知識(shí)。也正是基礎(chǔ),構(gòu)成了前端知識(shí)體系的方方面面。

我是依揚(yáng),螞蟻集團(tuán)-保險(xiǎn)團(tuán)隊(duì)正在大量招聘中,詳情見:我們是螞蟻保險(xiǎn)前端團(tuán)隊(duì),我們今年在做什么,有興趣快來聯(lián)系我吧[email protected]
