最新:Lodash 嚴重安全漏洞背後你不得不知道的 JavaScript 知識

可能有信息敏感的同學已經瞭解到: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 == 1true,服務被攻擊。

攻擊案例出自:Prototype pollution attacks in NodeJS applications

這樣的漏洞在 jQuery $.extend 中也經常見到:

對於 jQuery:如果擔心安全問題,建議升級至最新版本 jQuery 3.4.0,如果還在使用 jQuery 的 1.x 和 2.x 版本,那麼你的應用程序和網站仍有可能遭受攻擊。

防範原型污染

瞭解了漏洞潛在問題以及攻擊手段,那麼如何防範呢?

在 lodash “連夜”發版的修復中:

image.png

我們可以清晰的看到,在遍歷 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 數據結構
  • 深拷貝
  • 以及其他問題

這麼來看,全是基礎知識。也正是基礎,構成了前端知識體系的方方面面。

這篇文章靈感來源於我們的羣中的相關討論:

image.png

這是一個什麼羣呢?咳咳。。。到了我的廣告時間了,這是我的一個課程討論羣:前端開發核心知識進階

感興趣的讀者可以:

PC 端點擊瞭解更多《前端開發核心知識進階》

移動端點擊瞭解更多:

移動端掃碼瞭解更多《前端開發核心知識進階

大綱內容:

image

Happy coding!

參考:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章