預防NodeJS命令注入的方法詳解
前言
Node.js和npm為前端生態(tài)中提供了統(tǒng)一的開發(fā)語言、強大的包管理和模塊生態(tài)系統(tǒng)、靈活的構建工具和任務自動化、以及豐富的前端框架和庫等等。
可以說,正是因為nodejs帶來的這些工具和資源使前端開發(fā)更加高效、便捷,并推動了前端技術的快速發(fā)展。
但是近年來,Node.js 生態(tài)系統(tǒng)中的 npm 軟件包中出現(xiàn)了許多 CVE("Common Vulnerabilities and Exposures" 常見漏洞和公開漏洞),譬如lodash庫的CVE漏洞——CVE-2018-16487[2]、express庫的CVE漏洞——CVE-2018-17346[3]以及jsonwebtoken庫的CVE漏洞——CVE-2018-12424[4]等等,在這其中有一個特別危險且屢禁不止的漏洞就是命令注入(Command Injection)。
作為前端工程師而言,在我們日常工作中,不僅需要快速交付、優(yōu)化性能相關,還要時刻對項目中所采用的nodejs技術棧及其安全相關的因素考慮在內。
簡而言之,關于安全這根弦兒得時刻緊繃著!
命令注入[5]是一種攻擊,其目的是通過有漏洞的應用程序在主機操作系統(tǒng)上執(zhí)行任意命令。當應用程序將用戶提供的不安全數據(表單、cookie、HTTP 標頭等)傳遞給系統(tǒng)shell時,就有可能發(fā)生命令注入攻擊。在這種攻擊中,攻擊者提供的操作系統(tǒng)命令通常是以受攻擊應用程序的權限執(zhí)行的。命令注入攻擊之所以可能,主要是因為輸入校驗不足。
這種攻擊與代碼注入不同,代碼注入允許攻擊者添加自己的代碼,然后由應用程序執(zhí)行。在命令注入攻擊中,攻擊者擴展應用程序的默認功能,執(zhí)行系統(tǒng)命令,而無需注入代碼。
場景分析
假設有某程序員a同學在某個nodejs項目中寫出了類似的代碼:
const { exec } = require('child_process'); function runCommand(userInput) { const command = `ls ${userInput}`; // 將用戶所輸入的內容拼接到命令中 exec(command, (error, stdout, stderr) => { if (error) { console.error(`執(zhí)行命令時出錯:${error}`); return; } console.log(`命令執(zhí)行結果:${stdout}`); }); } const userInput = '; rm -rf /'; // 惡意用戶所輸入的內容 runCommand(userInput);
我們簡單分析下以上代碼,這段程序可以將用戶所輸入的內容直接拼接到命令行字符串中。如果因為項目工期緊張,沒經過code review匆忙上線,恰好碰到個別用戶所輸入的惡意的命令,例如'; rm -rf /',那么最終執(zhí)行的命令將變?yōu)閘s ; rm -rf /,導致“刪庫跑路”的危險操作。
當然這只是為了舉例的簡單例子,實際項目中,發(fā)生命令注入的原因大多是沒有對用戶所輸入的內容進行嚴謹的校驗。
命令注入 - 常見威脅
命令注入是 Node.js 生態(tài)系統(tǒng)中真實而普遍的威脅。
看似顯而易見的安全風險,如以下代碼所示:
var exec = require('child_process').execSync var platform = require('os').platform() module.exports = function(){ var commands = Array.isArray(arguments[0]) ? arguments[0] : Array.prototype.slice.apply(arguments) var command = null commands.some(function(c){ if (isExec(findCommand(c))){ command = c return true } }) return command } function isExec(command){ try{ exec(command, { stdio: 'ignore' }) return true } catch (_e){ return false } } function findCommand(command){ if (/^win/.test(platform)){ return "where " + command } else { return "command -v " + command } }
上述命令注入漏洞是在 find-exec[6] npm 軟件包中發(fā)現(xiàn)的,該軟件包每周的下載量多達 2000 多次。雖然數量不多,但足以讓一些用戶面臨風險。命令注入漏洞的后果可能是毀滅性的,從數據泄露到系統(tǒng)完全崩潰不等。
現(xiàn)在我們再回過頭來看,到底什么是命令注入[7]?簡而言之,命令注入的核心是應用程序允許未經審核的用戶所輸入的內容作為系統(tǒng)命令執(zhí)行。這些命令可操縱底層系統(tǒng),可能導致未經授權的訪問、數據泄露,甚至完全破壞系統(tǒng)。
fs-git
在另外一個案例中,我們可以看下fs-git npm 軟件包(版本 1.0.1)這個看似無害的模塊是如何成為一個嚴重的安全隱患的:
fs-git 是 Node.js 的一個 npm 包,能夠為 Git 倉庫提供類似于文件系統(tǒng)的 API,進而可以讓開發(fā)人員更直觀、更容易地與 Git 倉庫交互。它擁有相當數量的用戶群體,所以該安全隱患所造成的影響可見一斑。
在1.0.1 版本的 fs-git 模塊中,被發(fā)現(xiàn)了編號為 CVE-2017-1000451[8]漏洞。該模塊依賴 child_process.exec 函數來執(zhí)行系統(tǒng)命令。然而,用于構建執(zhí)行字符串的 buildCommand函數缺少嚴謹的校驗邏輯,使其容易受到命令注入的攻擊。
以下是fs-git 中存在漏洞的代碼片段:
showRef(): Promise<RefInfo[]> { let command = this._buildCommand("show-ref"); return new Promise((resolve: (value: RefInfo[]) => void, reject: (error: any) => void) => { child_process.exec(command, { maxBuffer: maxBuffer }, (error, stdout, stderr) => { if (error) { reject(error); } else { let list = stdout.toString("utf8").split("\n").filter(line => !!line); let resultList:RefInfo[] = list.map(str=> { let columns = str.split(" ", 2); 返回 { gitDir: this.path、 ref: columns[0]、 name: columns[1] }; }); resolve(resultList);
最終,代碼還將調用 _buildCommand 函數,其中包含字符串連接和用戶提供的數據:
_buildCommand(...args: string[]): string { return `git --git-dir=${this.path} ${args.join(" ") }`;
當攻擊者篡改傳遞給 fs-git 模塊的數據以制作利用命令注入漏洞的惡意代碼時,攻擊就展開了。通過提供精心制作的輸入,攻擊者能夠向系統(tǒng)注入任意命令。這樣,攻擊者就可以利用運行進程的權限執(zhí)行未經授權的命令,從而可能危及主機系統(tǒng)。
該漏洞影響深遠。攻擊者可以執(zhí)行任意命令,其中可能包括外泄敏感數據、修改文件甚至破壞系統(tǒng)正常運行等操作。對于依賴fs-git的項目和應用程序來說,這個漏洞構成了重大的安全風險。
這個案例充分說明了校驗用戶所輸入的內容和必要的數據清除在防止命令注入漏洞方面的重要性。
所以即使是看似無害的模塊,如果不遵循安全編碼實踐,也會帶來嚴重的安全風險。開發(fā)者在處理用戶所輸入的內容的數據時必須十分謹慎。
安全建議
對于NodeJs項目,我們可以大致從以下幾點入手,從而減少命令注入的風險:
- 使用ORM(對象關系映射)庫:使用ORM庫可以幫助處理數據庫查詢,避免手動拼接SQL語句,從而減少SQL注入的風險。
譬如,筆者就在曾經的Egg.js項目中使用過的Sequelize[9] ORM庫來執(zhí)行安全的數據庫操作。
- 校驗嚴謹
對用戶所輸入的內容進行校驗和過濾,以防止惡意輸入
- 遵循安全編碼規(guī)范
避免直接拼接用戶所輸入的內容到命令字符串、使用安全的文件路徑拼接方法等。確保在代碼中進行輸入校驗和輸出轉義,并注意處理用戶所輸入的內容時的邊界情況。
- NPM Audit & NSP
使用經過安全審計和更新頻繁的第三方庫,以減少潛在的安全漏洞。另外還可以使用工具如npm Audit[10]或NSP(Node Security Platform)[11]來檢查項目依賴的安全性。
回過頭看
假設項目中需要使用到exec[12]和spawn[13]方法時,如果沒有適當的數據清理和校驗,用戶所輸入的內容可能被惡意利用,導致命令注入攻擊。
以下是一個簡單Demo說明這些類似的場景:
const { exec, spawn } = require('child_process'); // 示例:使用exec執(zhí)行命令 function executeCommandWithExec(userInput) { const command = `ls ${userInput}`; // 拼接用戶所輸入的內容的命令 exec(command, (error, stdout, stderr) => { if (error) { console.error(`執(zhí)行命令出錯:${error}`); return; } console.log(`命令執(zhí)行結果:${stdout}`); }); } // 示例:使用spawn執(zhí)行命令 function executeCommandWithSpawn(userInput) { const command = 'ls'; const args = [userInput]; // 將用戶所輸入的內容作為命令行參數 const child = spawn(command, args); child.stdout.on('data', (data) => { console.log(`命令執(zhí)行結果:${data}`); }); child.stderr.on('data', (data) => { console.error(`執(zhí)行命令出錯:${data}`); }); } // 測試示例 const userInput = '; rm -rf /'; // 惡意的用戶所輸入的內容,嘗試刪除整個系統(tǒng) executeCommandWithExec(userInput); executeCommandWithSpawn(userInput);
在上面的示例中,executeCommandWithExec和executeCommandWithSpawn函數接受用戶所輸入的內容,并將其用于執(zhí)行l(wèi)s命令。
然而,如果惡意用戶所輸入的內容像; rm -rf /這樣的內容,它會將rm -rf /命令添加到ls命令后面,進而導致"刪庫跑路"的悲劇發(fā)生。
為了防止這種攻擊,應該對用戶所輸入的內容進行適當的數據清理和校驗。
所以對于以上代碼,可以使用安全的執(zhí)行方法execFile和spawn,并將用戶所輸入的內容作為命令行參數而不是直接拼接到命令中:
const { execFile, spawn } = require('child_process'); // 示例:使用execFile執(zhí)行命令 function executeCommandWithExecFile(userInput) { const command = 'ls'; const args = [userInput]; // 將用戶所輸入的內容作為命令行參數 execFile(command, args, (error, stdout, stderr) => { if (error) { console.error(`執(zhí)行命令出錯:${error}`); return; } console.log(`命令執(zhí)行結果:${stdout}`); }); } // 示例:使用spawn執(zhí)行命令 function executeCommandWithSpawn(userInput) { const command = 'ls'; const args = [userInput]; // 將用戶所輸入的內容作為命令行參數 const child = spawn(command, args); child.stdout.on('data', (data) => { console.log(`命令執(zhí)行結果:${data}`); }); child.stderr.on('data', (data) => { console.error(`執(zhí)行命令出錯:${data}`); }); } // 測試示例 const userInput = '; rm -rf /'; // 惡意的用戶所輸入的內容,嘗試刪除整個系統(tǒng) executeCommandWithExecFile(userInput); executeCommandWithSpawn(userInput);
在上面的代碼實現(xiàn)中,executeCommandWithExecFile函數使用了execFile[14]方法來執(zhí)行命令,而executeCommandWithSpawn函數保持不變,仍然使用spawn方法執(zhí)行命令。
使用execFile方法可以避免將用戶所輸入的內容直接拼接到命令中,這樣可以在一定程度上減少命令注入攻擊的風險。
總結
記住,無論采用哪種方案,主體思想都應該是謹慎處理用戶所輸入的內容,并進行嚴謹的校驗,以確保代碼的安全性。
以上就是預防NodeJS命令注入的方法詳解的詳細內容,更多關于預防NodeJS命令注入的資料請關注腳本之家其它相關文章!
相關文章
基于html5和nodejs相結合實現(xiàn)websocket即使通訊
HTML5 擁有許多引人注目的新特性,如 Canvas、本地存儲、多媒體編程接口、WebSocket 等等。雖然現(xiàn)在大家把它捧的很火的樣子,但是個人認為它還需要其他平臺的支持才能真正的"火起來"2015-11-11