預(yù)防NodeJS命令注入的方法詳解
前言
Node.js和npm為前端生態(tài)中提供了統(tǒng)一的開發(fā)語言、強大的包管理和模塊生態(tài)系統(tǒng)、靈活的構(gòu)建工具和任務(wù)自動化、以及豐富的前端框架和庫等等。
可以說,正是因為nodejs帶來的這些工具和資源使前端開發(fā)更加高效、便捷,并推動了前端技術(shù)的快速發(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)。
作為前端工程師而言,在我們?nèi)粘9ぷ髦校粌H需要快速交付、優(yōu)化性能相關(guān),還要時刻對項目中所采用的nodejs技術(shù)棧及其安全相關(guān)的因素考慮在內(nèi)。
簡而言之,關(guān)于安全這根弦兒得時刻緊繃著!
命令注入[5]是一種攻擊,其目的是通過有漏洞的應(yīng)用程序在主機操作系統(tǒng)上執(zhí)行任意命令。當(dāng)應(yīng)用程序?qū)⒂脩籼峁┑牟话踩珨?shù)據(jù)(表單、cookie、HTTP 標頭等)傳遞給系統(tǒng)shell時,就有可能發(fā)生命令注入攻擊。在這種攻擊中,攻擊者提供的操作系統(tǒng)命令通常是以受攻擊應(yīng)用程序的權(quán)限執(zhí)行的。命令注入攻擊之所以可能,主要是因為輸入校驗不足。
這種攻擊與代碼注入不同,代碼注入允許攻擊者添加自己的代碼,然后由應(yīng)用程序執(zhí)行。在命令注入攻擊中,攻擊者擴展應(yīng)用程序的默認功能,執(zhí)行系統(tǒng)命令,而無需注入代碼。
場景分析
假設(shè)有某程序員a同學(xué)在某個nodejs項目中寫出了類似的代碼:
const { exec } = require('child_process');
function runCommand(userInput) {
const command = `ls ${userInput}`; // 將用戶所輸入的內(nèi)容拼接到命令中
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`執(zhí)行命令時出錯:${error}`);
return;
}
console.log(`命令執(zhí)行結(jié)果:${stdout}`);
});
}
const userInput = '; rm -rf /'; // 惡意用戶所輸入的內(nèi)容
runCommand(userInput);
我們簡單分析下以上代碼,這段程序可以將用戶所輸入的內(nèi)容直接拼接到命令行字符串中。如果因為項目工期緊張,沒經(jīng)過code review匆忙上線,恰好碰到個別用戶所輸入的惡意的命令,例如'; rm -rf /',那么最終執(zhí)行的命令將變?yōu)閘s ; rm -rf /,導(dǎo)致“刪庫跑路”的危險操作。
當(dāng)然這只是為了舉例的簡單例子,實際項目中,發(fā)生命令注入的原因大多是沒有對用戶所輸入的內(nèi)容進行嚴謹?shù)男r灐?/p>
命令注入 - 常見威脅
命令注入是 Node.js 生態(tài)系統(tǒng)中真實而普遍的威脅。
看似顯而易見的安全風(fē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 多次。雖然數(shù)量不多,但足以讓一些用戶面臨風(fēng)險。命令注入漏洞的后果可能是毀滅性的,從數(shù)據(jù)泄露到系統(tǒng)完全崩潰不等。
現(xiàn)在我們再回過頭來看,到底什么是命令注入[7]?簡而言之,命令注入的核心是應(yīng)用程序允許未經(jīng)審核的用戶所輸入的內(nèi)容作為系統(tǒng)命令執(zhí)行。這些命令可操縱底層系統(tǒng),可能導(dǎo)致未經(jīng)授權(quán)的訪問、數(shù)據(jù)泄露,甚至完全破壞系統(tǒng)。
fs-git
在另外一個案例中,我們可以看下fs-git npm 軟件包(版本 1.0.1)這個看似無害的模塊是如何成為一個嚴重的安全隱患的:
fs-git 是 Node.js 的一個 npm 包,能夠為 Git 倉庫提供類似于文件系統(tǒng)的 API,進而可以讓開發(fā)人員更直觀、更容易地與 Git 倉庫交互。它擁有相當(dāng)數(shù)量的用戶群體,所以該安全隱患所造成的影響可見一斑。
在1.0.1 版本的 fs-git 模塊中,被發(fā)現(xiàn)了編號為 CVE-2017-1000451[8]漏洞。該模塊依賴 child_process.exec 函數(shù)來執(zhí)行系統(tǒng)命令。然而,用于構(gòu)建執(zhí)行字符串的 buildCommand函數(shù)缺少嚴謹?shù)男r炦壿嫞蛊淙菀资艿矫钭⑷氲墓簟?/p>
以下是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);
最終,代碼還將調(diào)用 _buildCommand 函數(shù),其中包含字符串連接和用戶提供的數(shù)據(jù):
_buildCommand(...args: string[]): string {
return `git --git-dir=${this.path} ${args.join(" ") }`;
當(dāng)攻擊者篡改傳遞給 fs-git 模塊的數(shù)據(jù)以制作利用命令注入漏洞的惡意代碼時,攻擊就展開了。通過提供精心制作的輸入,攻擊者能夠向系統(tǒng)注入任意命令。這樣,攻擊者就可以利用運行進程的權(quán)限執(zhí)行未經(jīng)授權(quán)的命令,從而可能危及主機系統(tǒng)。
該漏洞影響深遠。攻擊者可以執(zhí)行任意命令,其中可能包括外泄敏感數(shù)據(jù)、修改文件甚至破壞系統(tǒng)正常運行等操作。對于依賴fs-git的項目和應(yīng)用程序來說,這個漏洞構(gòu)成了重大的安全風(fēng)險。
這個案例充分說明了校驗用戶所輸入的內(nèi)容和必要的數(shù)據(jù)清除在防止命令注入漏洞方面的重要性。
所以即使是看似無害的模塊,如果不遵循安全編碼實踐,也會帶來嚴重的安全風(fēng)險。開發(fā)者在處理用戶所輸入的內(nèi)容的數(shù)據(jù)時必須十分謹慎。
安全建議
對于NodeJs項目,我們可以大致從以下幾點入手,從而減少命令注入的風(fēng)險:
- 使用ORM(對象關(guān)系映射)庫:使用ORM庫可以幫助處理數(shù)據(jù)庫查詢,避免手動拼接SQL語句,從而減少SQL注入的風(fēng)險。
譬如,筆者就在曾經(jīng)的Egg.js項目中使用過的Sequelize[9] ORM庫來執(zhí)行安全的數(shù)據(jù)庫操作。
- 校驗嚴謹
對用戶所輸入的內(nèi)容進行校驗和過濾,以防止惡意輸入
- 遵循安全編碼規(guī)范
避免直接拼接用戶所輸入的內(nèi)容到命令字符串、使用安全的文件路徑拼接方法等。確保在代碼中進行輸入校驗和輸出轉(zhuǎn)義,并注意處理用戶所輸入的內(nèi)容時的邊界情況。
- NPM Audit & NSP
使用經(jīng)過安全審計和更新頻繁的第三方庫,以減少潛在的安全漏洞。另外還可以使用工具如npm Audit[10]或NSP(Node Security Platform)[11]來檢查項目依賴的安全性。
回過頭看
假設(shè)項目中需要使用到exec[12]和spawn[13]方法時,如果沒有適當(dāng)?shù)臄?shù)據(jù)清理和校驗,用戶所輸入的內(nèi)容可能被惡意利用,導(dǎo)致命令注入攻擊。
以下是一個簡單Demo說明這些類似的場景:
const { exec, spawn } = require('child_process');
// 示例:使用exec執(zhí)行命令
function executeCommandWithExec(userInput) {
const command = `ls ${userInput}`; // 拼接用戶所輸入的內(nèi)容的命令
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`執(zhí)行命令出錯:${error}`);
return;
}
console.log(`命令執(zhí)行結(jié)果:${stdout}`);
});
}
// 示例:使用spawn執(zhí)行命令
function executeCommandWithSpawn(userInput) {
const command = 'ls';
const args = [userInput]; // 將用戶所輸入的內(nèi)容作為命令行參數(shù)
const child = spawn(command, args);
child.stdout.on('data', (data) => {
console.log(`命令執(zhí)行結(jié)果:${data}`);
});
child.stderr.on('data', (data) => {
console.error(`執(zhí)行命令出錯:${data}`);
});
}
// 測試示例
const userInput = '; rm -rf /'; // 惡意的用戶所輸入的內(nèi)容,嘗試刪除整個系統(tǒng)
executeCommandWithExec(userInput);
executeCommandWithSpawn(userInput);
在上面的示例中,executeCommandWithExec和executeCommandWithSpawn函數(shù)接受用戶所輸入的內(nèi)容,并將其用于執(zhí)行l(wèi)s命令。
然而,如果惡意用戶所輸入的內(nèi)容像; rm -rf /這樣的內(nèi)容,它會將rm -rf /命令添加到ls命令后面,進而導(dǎo)致"刪庫跑路"的悲劇發(fā)生。
為了防止這種攻擊,應(yīng)該對用戶所輸入的內(nèi)容進行適當(dāng)?shù)臄?shù)據(jù)清理和校驗。
所以對于以上代碼,可以使用安全的執(zhí)行方法execFile和spawn,并將用戶所輸入的內(nèi)容作為命令行參數(shù)而不是直接拼接到命令中:
const { execFile, spawn } = require('child_process');
// 示例:使用execFile執(zhí)行命令
function executeCommandWithExecFile(userInput) {
const command = 'ls';
const args = [userInput]; // 將用戶所輸入的內(nèi)容作為命令行參數(shù)
execFile(command, args, (error, stdout, stderr) => {
if (error) {
console.error(`執(zhí)行命令出錯:${error}`);
return;
}
console.log(`命令執(zhí)行結(jié)果:${stdout}`);
});
}
// 示例:使用spawn執(zhí)行命令
function executeCommandWithSpawn(userInput) {
const command = 'ls';
const args = [userInput]; // 將用戶所輸入的內(nèi)容作為命令行參數(shù)
const child = spawn(command, args);
child.stdout.on('data', (data) => {
console.log(`命令執(zhí)行結(jié)果:${data}`);
});
child.stderr.on('data', (data) => {
console.error(`執(zhí)行命令出錯:${data}`);
});
}
// 測試示例
const userInput = '; rm -rf /'; // 惡意的用戶所輸入的內(nèi)容,嘗試刪除整個系統(tǒng)
executeCommandWithExecFile(userInput);
executeCommandWithSpawn(userInput);
在上面的代碼實現(xiàn)中,executeCommandWithExecFile函數(shù)使用了execFile[14]方法來執(zhí)行命令,而executeCommandWithSpawn函數(shù)保持不變,仍然使用spawn方法執(zhí)行命令。
使用execFile方法可以避免將用戶所輸入的內(nèi)容直接拼接到命令中,這樣可以在一定程度上減少命令注入攻擊的風(fēng)險。
總結(jié)
記住,無論采用哪種方案,主體思想都應(yīng)該是謹慎處理用戶所輸入的內(nèi)容,并進行嚴謹?shù)男r灒源_保代碼的安全性。
以上就是預(yù)防NodeJS命令注入的方法詳解的詳細內(nèi)容,更多關(guān)于預(yù)防NodeJS命令注入的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Nodejs回調(diào)加超時限制兩種實現(xiàn)方法
這篇文章主要介紹了Nodejs回調(diào)加超時限制兩種實現(xiàn)方法的相關(guān)資料,需要的朋友可以參考下2017-06-06
基于html5和nodejs相結(jié)合實現(xiàn)websocket即使通訊
HTML5 擁有許多引人注目的新特性,如 Canvas、本地存儲、多媒體編程接口、WebSocket 等等。雖然現(xiàn)在大家把它捧的很火的樣子,但是個人認為它還需要其他平臺的支持才能真正的"火起來"2015-11-11

