FlawOpen

flawopen.com/命令注入/Node child_process.exec

Node.js child_process.exec() 命令注入风险

参考页面
通俗解释

把命令字符串传给 exec(),就像把一句话交给排版人员,并让标点决定指令在哪里分开。如果不可信文本含有 shell 语法,shell 可能会把它当作控制指令。execFile() 会传入固定程序和独立参数,默认不经过 shell 解析。

有风险
const { exec } = require('node:child_process');

function inspectFile(filename) {
  return exec(`file ${filename}`);
}
已修复
const { execFile } = require('node:child_process');

const allowed = new Map([
  ['report-1', '/srv/reports/1.pdf'],
  ['report-2', '/srv/reports/2.pdf']
]);

function inspectReport(reportId) {
  const path = allowed.get(reportId);
  if (!path) throw new Error('Unknown report');
  return execFile('/usr/bin/file', ['--', path], {
    shell: false,
    timeout: 3000,
    maxBuffer: 64 * 1024
  });
}

execFile() 如何降低命令注入风险

exec() 会启动 shell 并向其传递一条命令字符串。如果代码将攻击者可控文本插入该字符串,shell 就会把元字符解析为语法,这正是 CWE-78 所描述的命令注入路径。execFile() 使用参数数组直接启动指定可执行文件,默认不创建 shell,因此 shell 不会再次解析参数中的分隔符。示例还将调用方 ID 映射到固定路径。但这不意味着目标程序一定安全:应校验程序接受的参数;若支持,可使用 -- 结束选项解析;不要对不可信值启用 shell: true。

如何查找不安全的 exec() 调用

搜索 exec() 和 execSync(),并追踪每条命令字符串到请求参数、数据库字段、文件名和环境变量。记录可执行文件及各参数的边界。优先使用直接库 API;否则使用固定可执行文件、经过校验的参数、运行时间限制和输出上限,并调用 execFile()。

grep -RInE 'exec(Sync)?[[:space:]]*\(' --include='*.js' --include='*.cjs' --include='*.mjs' --include='*.ts' .

同步版本

execSync() 呢?

execSync() 使用同样基于 shell 的命令字符串,具有相同的注入风险,还会阻塞事件循环。execFileSync() 默认避免 shell 解析,但仍需校验可执行文件和参数。

参考资料

另请参阅 操作系统命令注入(CWE-78):原理及在各种语言中的防范 — flawopen.com Node.js exec() 与 execFile():区别与安全用法 Java Runtime.exec() 与 Command Injection:区别是什么?