Node.Js命令执行漏洞(CVE-2021-21315)复现及排查
Node.Js命令执行漏洞(CVE-2021-21315)复现及排查
一、背景与问题
在2021年,Node.js官方发布了一个严重的安全漏洞(CVE-2021-21315),该漏洞允许攻击者通过构造特定的字符串参数,绕过常规的命令注入防护机制,最终实现任意命令执行。该漏洞的核心在于Node.js的child_process模块中对用户输入参数的处理存在安全缺陷。
该漏洞的触发条件是:当使用child_process.exec或child_process.execFile时,若用户输入参数未经过严格的转义处理,攻击者可以通过构造特殊字符(如$、&等)来注入恶意命令。这种漏洞在Web应用中尤其危险,因为用户输入通常来自HTTP请求参数、文件上传内容或数据库查询字段。
二、基本原理
Node.js的child_process模块用于执行外部命令,其核心函数包括:
child_process.exec(command, [options], callback):执行命令并返回输出child_process.execFile(file, [args], [options], callback):执行指定文件(如可执行文件)
这两个函数的共同问题是:未对用户输入参数进行严格的转义处理。特别是在使用shell: true选项时,会将命令视为shell命令进行解析,从而允许注入特殊语法。
漏洞触发条件
- 用户输入中包含特殊字符(如
$、&、|等) - 使用
shell: true选项(默认情况下exec会启用) - 命令参数未经过转义处理
漏洞影响
攻击者可以构造如ls; rm -rf /的命令,通过注入分号实现多个命令执行,最终导致服务器文件被删除。
三、环境准备
确保Node.js版本在受影响范围(v14.17.0至v16.10.0)。可以使用以下命令检查版本:
node -v如果版本在漏洞范围内,可复现漏洞。建议在安全测试环境中进行实验。
四、核心实现
1. 漏洞复现代码
const { exec } = require('child_process');
// 漏洞触发代码
exec('ls; echo "恶意命令注入"', (err, stdout, stderr) => {
if (err) {
console.error(`执行错误: ${err.message}`);
return;
}
console.log(`输出: ${stdout}`);
});关键代码解释:
exec函数默认启用shell: true,允许解析特殊字符ls; echo "恶意命令注入"中使用分号注入了额外命令- 攻击者可构造更危险的命令(如
ls; rm -rf /)
运行结果:
输出: (文件列表)
恶意命令注入2. 修复代码(使用转义)
const { exec } = require('child_process');
const { escape } = require('shell-quote');
// 安全处理用户输入
const userInput = "恶意输入; rm -rf /";
const escapedInput = escape(userInput);
exec(escapedInput, (err, stdout, stderr) => {
if (err) {
console.error(`执行错误: ${err.message}`);
return;
}
console.log(`输出: ${stdout}`);
});关键代码解释:
- 使用
shell-quote库的escape函数对用户输入进行转义 - 转义后的字符串会将特殊字符转义为安全形式(如
$转义为\$)
3. 安全执行方式(使用execFile)
const { execFile } = require('child_process');
// 安全执行方式
execFile('ls', ['--help'], (err, stdout, stderr) => {
if (err) {
console.error(`执行错误: ${err.message}`);
return;
}
console.log(`输出: ${stdout}`);
});关键代码解释:
- 使用
execFile直接指定可执行文件路径,避免通过shell解析 - 可执行文件路径需严格校验,防止路径遍历攻击
五、完整案例
场景:Web API接口中的命令执行漏洞
前端代码(Express.js):
const express = require('express');
const { exec } = require('child_process');
const app = express();
app.get('/execute', (req, res) => {
const userInput = req.query.cmd; // 获取用户输入
try {
exec(`echo ${userInput}`, (err, stdout, stderr) => {
if (err) {
res.status(500).send('执行错误');
return;
}
res.send(stdout);
});
} catch (e) {
res.status(500).send('服务器错误');
}
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});漏洞分析:
- 用户输入直接拼接到命令中,未进行转义
- 攻击者可输入
ls; rm -rf /触发危险命令
修复方案:
const { exec } = require('child_process');
const { escape } = require('shell-quote');
app.get('/execute', (req, res) => {
const userInput = req.query.cmd;
try {
const escapedInput = escape(userInput);
exec(escapedInput, (err, stdout, stderr) => {
if (err) {
res.status(500).send('执行错误');
return;
}
res.send(stdout);
});
} catch (e) {
res.status(500).send('服务器错误');
}
});运行结果:
- 输入
ls; rm -rf /将被转义为ls;\ rm\ -rf\ /,仅输出文件列表
六、源码解析
1. shell-quote库的转义机制
shell-quote库通过正则表达式将特殊字符转义:
// 转义函数核心逻辑
function escape(str) {
return str.replace(/([\$`"\\])/g, '\\$1');
}\$:转义$符号\$:转义反引号\":转义双引号\\:转义反斜杠
2. Node.js exec函数的shell处理
// child_process源码片段(简化)
void exec(const char *command, int options) {
if (options & SHELL_FLAG) {
parse_shell(command); // 解析shell命令
} else {
parse_direct(command); // 直接执行命令
}
}- 当
shell: true时,会调用parse_shell解析特殊语法 - 漏洞的核心在于未对用户输入进行严格校验
七、进阶使用
1. 使用exec的shell: false选项
exec('ls', { shell: false }, (err, stdout, stderr) => {
// 不启用shell解析,避免命令注入
});2. 使用execFile的shell: false选项
execFile('ls', ['--help'], { shell: false }, (err, stdout, stderr) => {
// 直接执行可执行文件,避免shell解析
});3. 使用第三方安全库
const { exec } = require('child_process');
const { escape } = require('shell-quote');
// 安全执行
exec(escape(userInput), (err, stdout, stderr) => {
// 安全处理
});八、性能与工程实践
1. 性能优化
- 避免频繁命令执行:将重复命令缓存或预处理
- 限制命令执行时间:使用
timeout参数控制执行时间 - 异步处理:使用
Promise或async/await避免阻塞主线程
const { exec } = require('child_process');
async function safeExec(cmd) {
return new Promise((resolve, reject) => {
exec(cmd, (err, stdout, stderr) => {
if (err) {
reject(err);
return;
}
resolve(stdout);
});
});
}2. 异常处理
- 捕获异常:使用
try/catch或回调函数处理错误 - 日志记录:记录异常命令和用户输入
- 速率限制:防止DDoS攻击(如限制单位时间内的命令执行次数)
九、常见问题与踩坑
1. 常见错误:未转义特殊字符
错误代码:
exec(`ls ${userInput}`);问题:userInput中可能包含$、;等特殊字符
修复方案:使用escape函数转义输入
2. 常见错误:错误使用shell: true
错误代码:
exec('ls; rm -rf /', { shell: true });问题:shell: true导致命令注入
修复方案:禁用shell: true或使用execFile
3. 常见错误:未校验输入路径
错误代码:
execFile(userInput, [], { shell: false });问题:userInput可能包含../等路径遍历字符
修复方案:严格校验输入路径,避免路径遍历
十、最佳实践
- 避免使用
shell: true:除非绝对必要,否则禁用shell解析 - 严格校验输入:使用正则表达式校验输入格式
- 使用安全库:优先使用
shell-quote等安全库处理输入 - 限制命令执行:使用
timeout和maxBuffer控制执行范围 - 日志记录:记录所有命令执行记录,便于审计
- 输入白名单:对关键输入字段使用白名单校验
十一、总结
CVE-2021-21315漏洞暴露了Node.js在命令执行安全方面的不足,其根本原因在于未对用户输入进行严格的转义处理。通过分析漏洞原理、复现漏洞、修复方案以及实际案例,我们可以深入理解命令执行的安全风险。
在实际开发中,应严格遵循以下原则:
- 禁止直接拼接用户输入:使用安全库处理输入
- 禁用不必要的shell解析:优先使用
execFile等安全方式 - 严格校验输入路径:防止路径遍历攻击
- 记录异常日志:便于后续安全审计
通过合理的设计和安全措施,可以有效规避命令执行漏洞带来的风险,确保系统的安全性和稳定性。
评论已关闭