麻烦出在哪里
运维工具类系统常需要调用外部命令:图片处理、格式转换、日志打包、备份脚本。风险不在于『调用命令』本身,而在于参数被拼接进命令行字符串——一旦用户输入混进命令串,就能改变命令语义。稳妥做法是建立统一封装:命令白名单、参数以数组形式传递、超时与输出限制、执行账号降权。
基本信息
| 常见语言 | Python subprocess、Java ProcessBuilder、Node child_process |
|---|---|
| 核心原则 | 参数以数组传值,不经 shell 解释 |
| 禁止用法 | shell=True、字符串拼接命令、eval 式执行 |
| 资源限制 | 超时、输出大小上限、CPU 与内存约束 |
| 权限要求 | 专用低权限账号运行,限定工作目录 |
典型使用场景
- 图像处理:生成缩略图、格式转换、水印叠加
- 文档转换:Office 转 PDF、PDF 合并与拆分
- 备份与打包:调用 tar、rsync、mysqldump 等工具
- 音视频处理:调用 ffmpeg 转码或截帧
- 运维脚本:调用系统工具采集信息,如容量与状态检查
统一封装示例
统一封装示例
import subprocess
ALLOWED = {'convert', 'ffmpeg', 'tar'} # 允许调用的命令白名单
def run_tool(tool, args, timeout=30, max_out=1_000_000):
if tool not in ALLOWED:
raise ValueError('tool not allowed') # 不在白名单直接拒绝
proc = subprocess.run([tool, *args], # 数组传参,不经 shell
capture_output=True, timeout=timeout, check=False,
cwd='/srv/tmp') # 固定工作目录
if len(proc.stdout) > max_out:
raise RuntimeError('output too large')
return proc.returncode, proc.stdout写法与风险对照
| 写法 | 风险 | 替代做法 |
|---|---|---|
| shell 字符串拼接 | 输入可改变命令语义 | 数组传参,参数作为独立元素 |
| 使用 shell=True | 管道与重定向符号被解释 | 关闭 shell,改用语言内置能力 |
| 命令名来自用户输入 | 任意命令执行 | 命令名固定,仅参数可控 |
| 无超时无输出限制 | 进程挂起、磁盘被写满 | 设超时与输出上限 |
| 以 root 执行 | 一旦被利用即最高权限 | 专用低权限账号,限定目录 |
把命令执行集中到单一封装函数,便于审计与静态检查:检索代码中的进程调用点,确认全部走了封装,而不是散落在业务逻辑里。
即使参数来自内部系统,也要按不可信输入对待:上游接口被污染时,内部参数照样会带上攻击载荷,白名单与超时是最后一道防线。