摘要: 詳解原型污染。前端
Fundebug經受權轉載,版權歸原做者全部。jquery
可能有信息敏感的同窗已經瞭解到:Lodash 庫爆出嚴重安全漏洞,波及 400萬+ 項目。這個漏洞使得 lodash 「連夜」發版以解決潛在問題,並強烈建議開發者升級版本。git
咱們在忙着「看熱鬧」或者「」升級版本」的同時,靜下心來想:真的有理解這個漏洞產生的緣由,明白漏洞修復背後的原理了嗎?github
這篇短文將從原理層面分析這一事件,相信「小白」讀者會有所收穫。express
其實漏洞很簡單,舉一個例子:lodash 中 defaultsDeep 方法,編程
_.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } })
輸出:json
{ '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 此次漏洞算是比較嚴重了。
由上分析,咱們知道原型污染並非什麼新鮮的漏洞,它「隨時可見」,「隨處可見」。在 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"
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對一、微脈、青團社等衆多品牌企業。歡迎你們免費試用!