19. 命令执行安全与进程隔离

本章解决的问题

child_process 能执行操作系统程序,也把命令注入、越权文件访问、资源耗尽和敏感信息泄漏带进了应用边界。参数数组能显著降低 Shell 注入风险,但它不是完整安全方案。

最危险的写法

import { exec } from "node:child_process";

exec(`python analyze.py ${request.body.inputPath}`);

当用户输入被拼进 Shell 命令时,Shell 会解释引号、分号、管道、重定向和命令替换。攻击者可能追加其他命令。Windows cmd.exe、PowerShell 和 Bash 的元字符不同,手工转义很难长期正确。

推荐:程序和参数分离

import * as path from "node:path";
import { spawn } from "node:child_process";

const inputRoot = path.resolve("data/uploads");
const requestedPath = path.resolve(inputRoot, request.body.fileName);
const relativePath = path.relative(inputRoot, requestedPath);

if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
  throw new Error("输入路径越出允许目录");
}

const child = spawn("python", [
  "scripts/analyze.py",
  "--input",
  requestedPath,
], {
  shell: false,
  cwd: path.resolve("runtime/analyzer"),
  env: {
    PATH: process.env.PATH ?? "",
    LANG: "C.UTF-8",
  },
  stdio: ["ignore", "pipe", "pipe"],
});

spawn(command, args, { shell: false }) 不让 Shell 重新解释普通参数,因此文件名即使包含空格,也作为一个参数传递。不要为了“命令能跑”随意设置 shell: true

参数数组不能解决什么

即使不经过 Shell,仍需防范:

命令和允许的选项应由服务端白名单决定,用户只提供经过验证的业务值。

路径校验的边界

path.relative() 检查能防止普通路径越界,但高风险场景还要考虑符号链接和检查后替换(TOCTOU)。可采用:

环境变量最小化

const childEnvironment: NodeJS.ProcessEnv = {
  PATH: process.env.PATH,
  LANG: process.env.LANG ?? "C.UTF-8",
  ANALYSIS_MODE: "safe",
};

不要默认传递包含数据库密码、云密钥、JWT Secret 的完整 process.env。同时注意 PATH 会影响实际执行到哪个程序;生产中可使用经过部署验证的绝对可执行文件路径,并记录工具版本。

输出、错误和资源上限

let stderrBytes = 0;
const maxStderrBytes = 1024 * 1024;

child.stderr.on("data", (chunk: Buffer) => {
  stderrBytes += chunk.length;
  if (stderrBytes <= maxStderrBytes) {
    process.stderr.write(chunk);
  }
});

还应同时限制:输入大小、并发子进程数、运行时间、stdout/stderr 保存量、临时目录总量和结果文件大小。Node.js 的超时与 kill() 只是应用层控制,强隔离需要操作系统用户、容器、cgroup、作业系统等协助。

stderr 可能暴露服务器路径、命令参数和数据内容。日志中应记录经过脱敏的工具名、任务 ID、退出码、Signal 和耗时,不应把子进程原始错误完整返回给终端用户。

安全执行时间线

接收业务参数
  → Schema 校验与大小限制
  → 服务端选择命令/允许参数
  → 创建受控任务目录
  → spawn(shell: false, env 白名单)
  → 流式消费受限输出
  → 超时/取消时终止并等待 close
  → 验证退出码与结果文件
  → 原子发布结果
  → 清理临时目录

任何一步失败都要进入有界清理,不能只返回 HTTP 错误而留下子进程继续运行。

可复现性也是生产安全

调用 Python、R、FFmpeg、samtools、bcftools 等程序时建议记录:

这能帮助判断“同一任务为什么得到不同结果”,也有利于审计和故障复盘。

跨平台注意事项

常见错误

练习

  1. 找出 exec(\convert ${fileName} output.pdf`)` 的风险,并改写为参数数组。
  2. 编写函数,验证解析后的输入路径必须位于指定根目录内。
  3. 为图片转换任务列出命令、参数、目录、环境、时间、并发和输出限制。
  4. 模拟子进程不断输出 stderr,验证日志保存量受到限制但流仍被消费。

验收清单